libowl 1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+