libowl 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,121 @@
1
+ ################################################################################
2
+ #This file contains functions to put data into and retrieve data from byte
3
+ #arrays when sending messages over a network.
4
+ #
5
+ # Copyright (c) 2013 Bernhard Firner
6
+ # All rights reserved.
7
+ #
8
+ # This program is free software; you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License
10
+ # as published by the Free Software Foundation; either version 2
11
+ # of the License, or (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program; if not, write to the Free Software
20
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21
+ # or visit http://www.gnu.org/licenses/gpl-2.0.html
22
+ #
23
+ ################################################################################
24
+
25
+ #Pack a 64 bit unsigned integer into a buffer
26
+ def packuint64(val)
27
+ return [val / 2**32].pack('N') + [val % 2**32].pack('N')
28
+ end
29
+
30
+ #Unpack a uint64_t big-endian integer from the buffer
31
+ def unpackuint64(buff)
32
+ high, low = buff.unpack('NN')
33
+ return high * 2**32 + low
34
+ end
35
+
36
+ #Pack a 128 bit unsigned integer into a buffer
37
+ def packuint128(val)
38
+ #TODO FIXME
39
+ #There is no 128 bit type in ruby so pad with zeros for now
40
+ return [0].pack('N') + [0].pack('N') + [val / 2**32].pack('N') + [val % 2**32].pack('N')
41
+ end
42
+
43
+ #Unpack a uint128_t big-endian integer from the buffer
44
+ def unpackuint128(buff)
45
+ #TODO FIXME
46
+ #There is no 128 bit type in ruby so pad with zeros for now
47
+ ignore1, ignore2, high, low = buff.unpack('NNNN')
48
+ return high * 2**32 + low
49
+ end
50
+
51
+ #Put a string into a buffer as a UTF16 string.
52
+ def strToUnicode(str)
53
+ unistr = ""
54
+ str.each_char { |c|
55
+ unistr << "\x00#{c}"
56
+ }
57
+ return unistr
58
+ end
59
+
60
+ #Put a string into a buffer as a UTF16 string and put the length of the string
61
+ #(in characters) at the beginning of the buffer as a 4-byte big-endian integer
62
+ def strToSizedUTF16(str)
63
+ buff = strToUnicode(str)
64
+ return "#{[buff.length].pack('N')}#{buff}"
65
+ end
66
+
67
+ #Read a sized UTF16 string (as encoded by the strToSizedUTF16 function) and
68
+ #return the string.
69
+ def readUTF16(buff)
70
+ len = buff.unpack('N')[0] / 2
71
+ rest = buff[4, buff.length - 1]
72
+ #puts "len is #{len} and rest is #{rest.length} bytes long"
73
+ str = ""
74
+ for i in 1..len do
75
+ if (rest.length >= 2)
76
+ #For now act as if the first byte will always be 0
77
+ c = rest.unpack('UU')[1]
78
+ rest = rest[2, rest.length - 1]
79
+ str << c
80
+ end
81
+ end
82
+ return str
83
+ end
84
+
85
+ def readUnsizedUTF16(buff)
86
+ len = buff.length / 2
87
+ rest = buff
88
+ #puts "len is #{len} and rest is #{rest.length} bytes long"
89
+ str = ""
90
+ for i in 1..len do
91
+ if (rest.length >= 2)
92
+ #For now act as if the first byte will always be 0
93
+ c = rest.unpack('UU')[1]
94
+ rest = rest[2, rest.length - 1]
95
+ str << c
96
+ end
97
+ end
98
+ return str
99
+ end
100
+
101
+
102
+ #Take in a buffer with a sized URI in UTF 16 format.
103
+ #Return the string that was at the beginning of the buffer and
104
+ #the rest of the buffer after the string
105
+ def splitURIFromRest(buff)
106
+ #The first four bytes are for the length of the string
107
+ strlen = buff.unpack('N')[0]
108
+ str = buff[0,strlen+4]
109
+ #Make another container for everything after the string
110
+ rest = buff[strlen+4,buff.length - 1]
111
+ if (rest == nil)
112
+ rest = []
113
+ end
114
+ if (strlen != 0)
115
+ return (readUTF16 str), rest
116
+ else
117
+ return '', rest
118
+ end
119
+ end
120
+
121
+
@@ -0,0 +1,434 @@
1
+ ################################################################################
2
+ #This file defines the ClientWorldConnection class, an object that represents
3
+ #a network connection to an Owl world model.
4
+ #
5
+ # Copyright (c) 2013 Bernhard Firner
6
+ # All rights reserved.
7
+ #
8
+ # This program is free software; you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License
10
+ # as published by the Free Software Foundation; either version 2
11
+ # of the License, or (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program; if not, write to the Free Software
20
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21
+ # or visit http://www.gnu.org/licenses/gpl-2.0.html
22
+ #
23
+ ################################################################################
24
+ require 'socket'
25
+ require 'libowl/message_constants.rb'
26
+ require 'libowl/wm_data.rb'
27
+ require 'libowl/buffer_manip.rb'
28
+ require 'libowl/response.rb'
29
+ require 'libowl/step_response.rb'
30
+
31
+ require 'thread'
32
+
33
+ ##
34
+ #A connection between a client and a world model.
35
+ #This class spawns a thread to handle incoming messages and returns
36
+ #instances of the Response and StepResponse classes to fulfill client
37
+ #requests. If a thread cannot be used then an instance of the class
38
+ #ClientWorldModel should be used instead.
39
+ class ClientWorldConnection
40
+ #Indicates if this object is successfully connected to a world model.
41
+ attr_reader :connected
42
+ @alias_to_attr_name
43
+ @alias_to_origin_name
44
+ #Data for outstanding requests. This is a map of lists with a nil entry
45
+ #inserted into the list when the request is complete. Other entries
46
+ #are maps of URIs to their attributes
47
+ @next_data
48
+ @request_errors
49
+ @connected
50
+ @promise_mutex
51
+ @cur_key
52
+ #Remember the order of URI searches. They do not use tickets so we must
53
+ #manage the order of URI search requests locally
54
+ @uri_search_keys
55
+ @single_response
56
+
57
+
58
+ ##
59
+ #Creates a new connection and spawns a thread to call handleMessage
60
+ #automatically.
61
+ def initialize(host, port)
62
+ @promise_mutex = Mutex.new
63
+ @cur_key = 0
64
+ @uri_search_keys = []
65
+ @single_response = {}
66
+ @connected = false
67
+ @host = host
68
+ @port = port
69
+ @socket = TCPSocket.open(host, port)
70
+ handshake = ""
71
+ ver_string = "GRAIL client protocol"
72
+ #The handshake is the length of the message, the protocol string, and the version (0).
73
+ handshake << [ver_string.length].pack('N') << ver_string << "\x00\x00"
74
+ #Send a handshake and then receive one
75
+ @socket.send(handshake, 0)
76
+ inshake = @socket.recvfrom(handshake.length)[0]
77
+ while (inshake.length < handshake.length)
78
+ #puts "Waiting for #{handshake.length - inshake.length} byte more of handshake."
79
+ inshake += @socket.recvfrom(handshake.length - inshake.length)[0]
80
+ end
81
+
82
+ @connected = true
83
+ for i in 1..handshake.length
84
+ if handshake[i] != inshake[i]
85
+ puts "Handshake failure!"
86
+ puts "For byte i we sent #{handshake[i]} but got #{inshake[i]}"
87
+ @connected = false
88
+ end
89
+ end
90
+
91
+ @alias_to_attr_name = {}
92
+ @alias_to_origin_name = {}
93
+ @next_data = {}
94
+ @request_errors = {}
95
+
96
+ #Start the listening thread
97
+ @listen_thread = Thread.new do
98
+ while (@connected)
99
+ handleMessage()
100
+ end
101
+ end
102
+ end
103
+
104
+ ##
105
+ #Close this connection
106
+ def close()
107
+ @socket.close()
108
+ @connected = false
109
+ end
110
+
111
+ ##
112
+ #Handle a message of currently unknown type. This is automatically
113
+ #handled by a thread spawned at object creation.
114
+ def handleMessage()
115
+ #puts "Handling message..."
116
+ #Get the message length as n unsigned integer
117
+ inlen = (@socket.recvfrom(4)[0]).unpack('N')[0]
118
+ inbuff = @socket.recvfrom(inlen)[0]
119
+ #Keep reading until the entire packet is read
120
+ #TODO This can block forever if a communication error occurs.
121
+ while (inbuff.length < inlen)
122
+ inbuff += @socket.recvfrom(inlen-inbuff.length)[0]
123
+ end
124
+ #Byte that indicates message type
125
+ control = inbuff.unpack('C')[0]
126
+ if control == ATTRIBUTE_ALIAS
127
+ decodeAttributeAlias(inbuff[1, inbuff.length - 1])
128
+ elsif control == ORIGIN_ALIAS
129
+ decodeOriginAlias(inbuff[1, inbuff.length - 1])
130
+ elsif control == REQUEST_COMPLETE
131
+ ticket = decodeTicketMessage(inbuff[1, inbuff.length-1])
132
+ #Mark the corresponding request as complete by appending a nil value
133
+ @promise_mutex.synchronize do
134
+ if (@next_data.has_key? ticket)
135
+ #Need an empty has in case a step response is waiting for a value
136
+ @next_data[ticket].push(nil)
137
+ end
138
+ end
139
+ elsif control == DATA_RESPONSE
140
+ data = decodeDataResponse(inbuff[1, inbuff.length - 1])
141
+ #If the request was cancelled then don't try to push any more data
142
+ @promise_mutex.synchronize do
143
+ if (@next_data.has_key? data.ticket)
144
+ @next_data[data.ticket][-1].store(data.uri, data.attributes)
145
+ #Add a new entry for the next value
146
+ if (not @single_response[data.ticket])
147
+ @next_data[data.ticket].push({})
148
+ end
149
+ end
150
+ end
151
+ elsif control == URI_RESPONSE
152
+ uris = decodeURIResponse(inbuff[1, inbuff.length - 1])
153
+ @promise_mutex.synchronize do
154
+ uri_ticket = @uri_search_keys.shift
155
+ puts "Finishing uri response for ticket #{uri_ticket}"
156
+ #Make world model entries with no attributes for each URI
157
+ uris.each{|uri| @next_data[uri_ticket][-1].store(uri, [])}
158
+ #This request is complete now so push a nil value to finish it
159
+ @next_data[uri_ticket].push(nil)
160
+ end
161
+ end
162
+ #puts "processed message with id #{control}"
163
+ return control
164
+ end
165
+
166
+
167
+ ##
168
+ #See if a request is still being serviced (only for StepResponse - regular
169
+ #requests can't be cancelled since they only have a single response).
170
+ def isComplete(key)
171
+ @promise_mutex.synchronize do
172
+ if ((not @next_data.has_key?(key)))
173
+ return true
174
+ elsif (@next_data[key].empty?)
175
+ return false
176
+ else
177
+ return (nil == @next_data[key][-1])
178
+ end
179
+ end
180
+ end
181
+
182
+ ##
183
+ #getNext should only be called if hasNext is true, otherwise
184
+ #the future will be given an exception since there is no data.
185
+ #The response and stepResponse classes can call this directly
186
+ #so there usually won't be a need for a developer to call this
187
+ #function directly.
188
+ def hasNext(key)
189
+ @promise_mutex.synchronize do
190
+ return ((@next_data.has_key? key) and (@next_data[key].length > 1))
191
+ end
192
+ end
193
+
194
+ ##
195
+ #Get the data from the response object corresponding to this key.
196
+ #The response and stepResponse classes can call this directly
197
+ #so there usually won't be a need for a developer to call this
198
+ #function directly.
199
+ def getNext(key)
200
+ if (not hasNext(key))
201
+ raise "No next value in request"
202
+ else
203
+ data = {}
204
+ @promise_mutex.synchronize do
205
+ data = @next_data[key].shift
206
+ end
207
+ #If there is no more data in this request delete its associatd data
208
+ if (isComplete(key))
209
+ @request_errors.delete key
210
+ @next_data.delete key
211
+ end
212
+ return data
213
+ end
214
+ end
215
+
216
+ ##
217
+ #Check if a request has an error.
218
+ #The response and stepResponse classes can call this directly
219
+ #so there usually won't be a need for a developer to call this
220
+ #function directly.
221
+ def hasError(key)
222
+ @promise_mutex.synchronize do
223
+ return (@request_errors.has_key? key)
224
+ end
225
+ end
226
+
227
+ ##
228
+ #Get error (will return std::exception("No error") is there is none
229
+ def getError(key)
230
+ if (not hasError(key))
231
+ raise "no error but getError called"
232
+ else
233
+ @promise_mutex.synchronize do
234
+ return @request_errors[key]
235
+ end
236
+ end
237
+ end
238
+
239
+ #Decode attribute alias message
240
+ def decodeAttributeAlias(inbuff)
241
+ num_aliases = inbuff.unpack('N')[0]
242
+ rest = inbuff[4, inbuff.length - 1]
243
+ for i in 1..num_aliases do
244
+ attr_alias = rest.unpack('N')[0]
245
+ name, rest = splitURIFromRest(rest[4, rest.length - 1])
246
+ #Assign this name to the given alias
247
+ @alias_to_attr_name[attr_alias] = name
248
+ end
249
+ end
250
+
251
+ #Decode origin alias message
252
+ def decodeOriginAlias(inbuff)
253
+ num_aliases = inbuff.unpack('N')[0]
254
+ rest = inbuff[4, inbuff.length - 1]
255
+ for i in 1..num_aliases do
256
+ origin_alias = rest.unpack('N')[0]
257
+ name, rest = splitURIFromRest(rest[4, rest.length - 1])
258
+ #Assign this name to the given alias
259
+ @alias_to_origin_name[origin_alias] = name
260
+ end
261
+ end
262
+
263
+ ##
264
+ #Decode a ticket message or a request complete message.
265
+ def decodeTicketMessage(inbuff)
266
+ return inbuff.unpack('N')[0]
267
+ end
268
+
269
+ ##
270
+ #Decode a URI response message, returning an array of WMData
271
+ def decodeURIResponse(inbuff)
272
+ uris = []
273
+ if (inbuff != nil)
274
+ rest = inbuff
275
+ while (rest.length > 4)
276
+ name, rest = splitURIFromRest(rest)
277
+ uris.push(name)
278
+ end
279
+ end
280
+ return uris
281
+ end
282
+
283
+ ##
284
+ #Decode a data response message, returning an array of WMData
285
+ def decodeDataResponse(inbuff)
286
+ attributes = []
287
+ object_uri, rest = splitURIFromRest(inbuff)
288
+ ticket = rest.unpack('N')[0]
289
+ total_attributes = rest[4, rest.length - 1].unpack('N')[0]
290
+ rest = rest[8, rest.length]
291
+ #puts "Decoding #{total_attributes} attributes"
292
+ for i in 1..total_attributes do
293
+ name_alias = rest.unpack('N')[0]
294
+ creation_date = unpackuint64(rest[4, rest.length - 1])
295
+ expiration_date = unpackuint64(rest[12, rest.length - 1])
296
+ origin_alias = rest[20, rest.length - 1].unpack('N')[0]
297
+ data_len = rest[24, rest.length - 1].unpack('N')[0]
298
+ data = rest[28, data_len]
299
+ rest = rest[28+data_len, rest.length - 1]
300
+ attributes.push(WMAttribute.new(@alias_to_attr_name[name_alias], data, creation_date, expiration_date, @alias_to_origin_name[origin_alias]))
301
+ end
302
+ return WMData.new(object_uri, attributes, ticket)
303
+ end
304
+
305
+ ##
306
+ #Issue a snapshot request, returning a Response object for the request.
307
+ def snapshotRequest(name_pattern, attribute_patterns, start_time = 0, stop_time = 0)
308
+ #Set up a ticket and mark this request as active by adding it to next_data
309
+ ticket = 0
310
+ @promise_mutex.synchronize do
311
+ ticket = @cur_key
312
+ @cur_key += 1
313
+ @single_response.store(ticket, true)
314
+ @next_data[ticket] = [{}]
315
+ end
316
+ buff = [SNAPSHOT_REQUEST].pack('C')
317
+
318
+ buff += [ticket].pack('N')
319
+ buff += strToSizedUTF16(name_pattern)
320
+ buff += [attribute_patterns.length].pack('N')
321
+
322
+ attribute_patterns.each{|pattern|
323
+ buff += strToSizedUTF16(pattern)
324
+ }
325
+
326
+ buff += packuint64(start_time)
327
+ buff += packuint64(stop_time)
328
+ #Send the message with its length prepended to the front
329
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
330
+ return Response.new(self, ticket)
331
+ end
332
+
333
+ ##
334
+ #Issue a range request, returning a Response object for the request.
335
+ def rangeRequest(name_pattern, attribute_patterns, start_time, stop_time)
336
+ #Set up a ticket and mark this request as active by adding it to next_data
337
+ ticket = 0
338
+ @promise_mutex.synchronize do
339
+ ticket = @cur_key
340
+ @cur_key += 1
341
+ @single_response.store(ticket, false)
342
+ @next_data[ticket] = [{}]
343
+ end
344
+ buff = [RANGE_REQUEST].pack('C')
345
+
346
+ buff += [ticket].pack('N')
347
+ buff += strToSizedUTF16(name_pattern)
348
+ buff += [attribute_patterns.length].pack('N')
349
+
350
+ attribute_patterns.each{|pattern|
351
+ buff += strToSizedUTF16(pattern)
352
+ }
353
+
354
+ buff += packuint64(start_time)
355
+ buff += packuint64(stop_time)
356
+
357
+ #Send the message with its length prepended to the front
358
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
359
+ return StepResponse.new(self, ticket)
360
+ end
361
+
362
+ ##
363
+ #Issue a stream request, returning a StepResponse object for the request.
364
+ def streamRequest(name_pattern, attribute_patterns, update_interval)
365
+ #Set up a ticket and mark this request as active by adding it to next_data
366
+ ticket = 0
367
+ @promise_mutex.synchronize do
368
+ ticket = @cur_key
369
+ @cur_key += 1
370
+ @single_response.store(ticket, false)
371
+ @next_data[ticket] = [{}]
372
+ end
373
+ buff = [STREAM_REQUEST].pack('C')
374
+
375
+ buff += [ticket].pack('N')
376
+ buff += strToSizedUTF16(name_pattern)
377
+ buff += [attribute_patterns.length].pack('N')
378
+
379
+ attribute_patterns.each{|pattern|
380
+ buff += strToSizedUTF16(pattern)
381
+ }
382
+
383
+ buff += packuint64(0)
384
+ buff += packuint64(update_interval)
385
+
386
+ #Send the message with its length prepended to the front
387
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
388
+ return StepResponse.new(self, ticket)
389
+ end
390
+
391
+ ##
392
+ #Search for any objects in the world model matching the given
393
+ #POSIX REGEX pattern.
394
+ def URISearch(name_pattern)
395
+ #Set up a ticket and mark this request as active by adding it to next_data
396
+ ticket = 0
397
+ @promise_mutex.synchronize do
398
+ ticket = @cur_key
399
+ @cur_key += 1
400
+ @single_response.store(ticket, true)
401
+ @next_data[ticket] = [{}]
402
+ @uri_search_keys.push(ticket)
403
+ end
404
+ buff = [URI_SEARCH].pack('C')
405
+ buff += strToUnicode(name_pattern)
406
+ #Send the message with its length prepended to the front
407
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
408
+ return Response.new(self, ticket)
409
+ end
410
+
411
+ ##
412
+ #Set a preference in the world model for certain origins.
413
+ def setOriginPreference(origin_weights)
414
+ buff = [ORIGIN_PREFERENCE].pack('C')
415
+ #Each origin weight should be a pair of a name and a value
416
+ origin_weights.each{|ow|
417
+ #It's okay to pack using N since this operates the same
418
+ #for signed and unsigned values.
419
+ buff += strToSizedUTF16(ow[0]) + [ow[1]].pack('N')
420
+ }
421
+ #Send the message with its length prepended to the front
422
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
423
+ end
424
+
425
+ ##Cancel a request with the given ticket number
426
+ def cancelRequest(ticket_number)
427
+ buff = [CANCEL_REQUEST].pack('C')
428
+ #Now append the ticket number as a 4 byte value
429
+ buff += [ticket_number].pack('N')
430
+ #Send the message with its length prepended to the front
431
+ @socket.send("#{[buff.length].pack('N')}#{buff}", 0)
432
+ end
433
+ end
434
+