Python implementation much improved. Can send requests now. Fully interoperable with Obj-C implementation's test cases.
1.1 --- a/BLIP/BLIPTest.m Tue Jun 03 22:24:21 2008 -0700
1.2 +++ b/BLIP/BLIPTest.m Wed Jun 04 17:11:20 2008 -0700
1.3 @@ -308,7 +308,7 @@
1.4 AssertEq(bytes[i],i % 256);
1.5
1.6 AssertEqual([request valueOfProperty: @"Content-Type"], @"application/octet-stream");
1.7 - AssertEqual([request valueOfProperty: @"User-Agent"], @"BLIPConnectionTester");
1.8 + Assert([request valueOfProperty: @"User-Agent"] != nil);
1.9 AssertEq([[request valueOfProperty: @"Size"] intValue], size);
1.10
1.11 [request respondWithData: body contentType: request.contentType];
2.1 --- a/Python/BLIP.py Tue Jun 03 22:24:21 2008 -0700
2.2 +++ b/Python/BLIP.py Wed Jun 04 17:11:20 2008 -0700
2.3 @@ -1,10 +1,9 @@
2.4 -#!/usr/bin/env python
2.5 # encoding: utf-8
2.6 """
2.7 BLIP.py
2.8
2.9 Created by Jens Alfke on 2008-06-03.
2.10 -Copyright (c) 2008 Jens Alfke. All rights reserved.
2.11 +Copyright notice and BSD license at end of file.
2.12 """
2.13
2.14 import asynchat
2.15 @@ -15,10 +14,17 @@
2.16 import struct
2.17 import sys
2.18 import traceback
2.19 -import unittest
2.20 import zlib
2.21
2.22
2.23 +# Connection status enumeration:
2.24 +kDisconnected = -1
2.25 +kClosed = 0
2.26 +kOpening = 1
2.27 +kOpen = 2
2.28 +kClosing = 3
2.29 +
2.30 +
2.31 # INTERNAL CONSTANTS -- NO TOUCHIES!
2.32
2.33 kFrameMagicNumber = 0x9B34F205
2.34 @@ -47,57 +53,118 @@
2.35 pass
2.36
2.37
2.38 +### LISTENER AND CONNECTION CLASSES:
2.39 +
2.40 +
2.41 class Listener (asyncore.dispatcher):
2.42 "BLIP listener/server class"
2.43
2.44 - def __init__(self, port):
2.45 + def __init__(self, port, sslKeyFile=None, sslCertFile=None):
2.46 "Create a listener on a port"
2.47 asyncore.dispatcher.__init__(self)
2.48 self.onConnected = self.onRequest = None
2.49 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
2.50 self.bind( ('',port) )
2.51 self.listen(5)
2.52 + self.sslKeyFile=sslKeyFile
2.53 + self.sslCertFile=sslCertFile
2.54 log.info("Listening on port %u", port)
2.55
2.56 def handle_accept( self ):
2.57 - client,address = self.accept()
2.58 - conn = Connection(address,client)
2.59 + socket,address = self.accept()
2.60 + if self.sslKeyFile:
2.61 + socket.ssl(socket,self.sslKeyFile,self.sslCertFile)
2.62 + conn = Connection(address, sock=socket, listener=self)
2.63 conn.onRequest = self.onRequest
2.64 if self.onConnected:
2.65 self.onConnected(conn)
2.66
2.67 + def handle_error(self):
2.68 + (typ,val,trace) = sys.exc_info()
2.69 + log.error("Listener caught: %s %s\n%s", typ,val,traceback.format_exc())
2.70 + self.close()
2.71 +
2.72 +
2.73
2.74 class Connection (asynchat.async_chat):
2.75 - def __init__( self, address, conn=None ):
2.76 + def __init__( self, address, sock=None, listener=None, ssl=None ):
2.77 "Opens a connection with the given address. If a connection/socket object is provided it'll use that,"
2.78 "otherwise it'll open a new outgoing socket."
2.79 - asynchat.async_chat.__init__(self,conn)
2.80 - self.address = address
2.81 - if conn:
2.82 + if sock:
2.83 + asynchat.async_chat.__init__(self,sock)
2.84 log.info("Accepted connection from %s",address)
2.85 + self.status = kOpen
2.86 else:
2.87 + asynchat.async_chat.__init__(self)
2.88 log.info("Opening connection to %s",address)
2.89 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
2.90 + self.status = kOpening
2.91 + if ssl:
2.92 + ssl(self.socket)
2.93 self.connect(address)
2.94 + self.address = address
2.95 + self.listener = listener
2.96 self.onRequest = None
2.97 self.pendingRequests = {}
2.98 self.pendingResponses = {}
2.99 self.outBox = []
2.100 self.inMessage = None
2.101 - self.inNumRequests = 0
2.102 + self.inNumRequests = self.outNumRequests = 0
2.103 self._endOfFrame()
2.104
2.105 - #def handle_error(self,x):
2.106 - # log.error("Uncaught exception: %s",x)
2.107 - # self.close()
2.108 + def close(self):
2.109 + if self.status > kClosed:
2.110 + self.status = kClosing
2.111 + log.info("Connection closing...")
2.112 + asynchat.async_chat.close(self)
2.113
2.114 - def _fatal(self, error):
2.115 - log.error("Fatal BLIP connection error: %s",error)
2.116 + def handle_connect(self):
2.117 + log.info("Connection open!")
2.118 + self.status = kOpen
2.119 +
2.120 + def handle_error(self):
2.121 + (typ,val,trace) = sys.exc_info()
2.122 + log.error("Connection caught: %s %s\n%s", typ,val,traceback.format_exc())
2.123 + self.discard_buffers()
2.124 + self.status = kDisconnected
2.125 self.close()
2.126
2.127 + def handle_close(self):
2.128 + log.info("Connection closed!")
2.129 + self.pendingRequests = self.pendingResponses = None
2.130 + self.outBox = None
2.131 + if self.status == kClosing:
2.132 + self.status = kClosed
2.133 + else:
2.134 + self.status = kDisconnected
2.135 + asynchat.async_chat.handle_close(self)
2.136 +
2.137
2.138 ### SENDING:
2.139
2.140 + @property
2.141 + def canSend(self):
2.142 + return self.status==kOpening or self.status==kOpen
2.143 +
2.144 + def _sendMessage(self, msg):
2.145 + if self.canSend:
2.146 + self._outQueueMessage(msg,True)
2.147 + return True
2.148 + else:
2.149 + return False
2.150 +
2.151 + def _sendRequest(self, req):
2.152 + if self.canSend:
2.153 + requestNo = req.requestNo = self.outNumRequests = self.outNumRequests + 1
2.154 + response = req.response
2.155 + if response:
2.156 + response.requestNo = requestNo
2.157 + self.pendingResponses[requestNo] = response
2.158 + log.debug("pendingResponses[%i] := %s",requestNo,response)
2.159 + return self._sendMessage(req)
2.160 + else:
2.161 + return False
2.162 +
2.163 def _outQueueMessage(self, msg,isNew=True):
2.164 n = len(self.outBox)
2.165 index = n
2.166 @@ -116,7 +183,7 @@
2.167
2.168 self.outBox.insert(index,msg)
2.169 if isNew:
2.170 - log.info("Queuing outgoing message at index %i",index)
2.171 + log.info("Queuing %s at index %i",msg,index)
2.172 if n==0:
2.173 self._sendNextFrame()
2.174 else:
2.175 @@ -144,7 +211,7 @@
2.176 self.inHeader = data
2.177 else:
2.178 self.inHeader += data
2.179 - else:
2.180 + elif self.inMessage:
2.181 self.inMessage._receivedData(data)
2.182
2.183 def found_terminator(self):
2.184 @@ -152,8 +219,8 @@
2.185 # Got a header:
2.186 (magic, requestNo, flags, frameLen) = struct.unpack(kFrameHeaderFormat,self.inHeader)
2.187 self.inHeader = None
2.188 - if magic!=kFrameMagicNumber: self._fatal("Incorrect frame magic number %x" %magic)
2.189 - if frameLen < kFrameHeaderSize: self._fatal("Invalid frame length %u" %frameLen)
2.190 + if magic!=kFrameMagicNumber: raise ConnectionException, "Incorrect frame magic number %x" %magic
2.191 + if frameLen < kFrameHeaderSize: raise ConnectionException,"Invalid frame length %u" %frameLen
2.192 frameLen -= kFrameHeaderSize
2.193 log.debug("Incoming frame: type=%i, number=%i, flags=%x, length=%i",
2.194 (flags&kMsgFlag_TypeMask),requestNo,flags,frameLen)
2.195 @@ -196,14 +263,14 @@
2.196 self.set_terminator(kFrameHeaderSize) # wait for binary header
2.197 if msg:
2.198 log.debug("End of frame of %s",msg)
2.199 - if not msg.moreComing:
2.200 + if not msg._moreComing:
2.201 self._receivedMessage(msg)
2.202
2.203 def _receivedMessage(self, msg):
2.204 log.info("Received: %s",msg)
2.205 # Remove from pending:
2.206 if msg.isResponse:
2.207 - del self.pendingReplies[msg.requestNo]
2.208 + del self.pendingResponses[msg.requestNo]
2.209 else:
2.210 del self.pendingRequests[msg.requestNo]
2.211 # Decode:
2.212 @@ -216,16 +283,17 @@
2.213 #FIX: Send an error reply
2.214
2.215
2.216 -### MESSAGES:
2.217 +### MESSAGE CLASSES:
2.218
2.219
2.220 class Message (object):
2.221 "Abstract superclass of all request/response objects"
2.222
2.223 - def __init__(self, connection, properties=None, body=None):
2.224 + def __init__(self, connection, body=None, properties=None):
2.225 self.connection = connection
2.226 + self.body = body
2.227 self.properties = properties or {}
2.228 - self.body = body
2.229 + self.requestNo = None
2.230
2.231 @property
2.232 def flags(self):
2.233 @@ -236,15 +304,17 @@
2.234 if self.urgent: flags |= kMsgFlag_Urgent
2.235 if self.compressed: flags |= kMsgFlag_Compressed
2.236 if self.noReply: flags |= kMsgFlag_NoReply
2.237 - if self.moreComing: flags |= kMsgFlag_MoreComing
2.238 + if self._moreComing:flags |= kMsgFlag_MoreComing
2.239 return flags
2.240
2.241 def __str__(self):
2.242 - s = "%s[#%i" %(type(self).__name__,self.requestNo)
2.243 + s = "%s[" %(type(self).__name__)
2.244 + if self.requestNo != None:
2.245 + s += "#%i" %self.requestNo
2.246 if self.urgent: s += " URG"
2.247 if self.compressed: s += " CMP"
2.248 if self.noReply: s += " NOR"
2.249 - if self.moreComing: s += " MOR"
2.250 + if self._moreComing:s += " MOR"
2.251 if self.body: s += " %i bytes" %len(self.body)
2.252 return s+"]"
2.253
2.254 @@ -253,12 +323,12 @@
2.255 if len(self.properties): s += repr(self.properties)
2.256 return s
2.257
2.258 - @property
2.259 + @property
2.260 def isResponse(self):
2.261 "Is this message a response?"
2.262 return False
2.263
2.264 - @property
2.265 + @property
2.266 def contentType(self):
2.267 return self.properties.get('Content-Type')
2.268
2.269 @@ -278,17 +348,19 @@
2.270 self.urgent = (flags & kMsgFlag_Urgent) != 0
2.271 self.compressed = (flags & kMsgFlag_Compressed) != 0
2.272 self.noReply = (flags & kMsgFlag_NoReply) != 0
2.273 - self.moreComing = (flags & kMsgFlag_MoreComing) != 0
2.274 + self._moreComing= (flags & kMsgFlag_MoreComing) != 0
2.275 self.frames = []
2.276
2.277 def _beginFrame(self, flags):
2.278 - if (flags & kMsgFlag_MoreComing)==0:
2.279 - self.moreComing = False
2.280 + """Received a frame header."""
2.281 + self._moreComing = (flags & kMsgFlag_MoreComing)!=0
2.282
2.283 def _receivedData(self, data):
2.284 + """Received data from a frame."""
2.285 self.frames.append(data)
2.286
2.287 def _finished(self):
2.288 + """The entire message has been received; now decode it."""
2.289 encoded = "".join(self.frames)
2.290 self.frames = None
2.291
2.292 @@ -327,47 +399,54 @@
2.293 '\x09' : "Error-Domain"}
2.294
2.295
2.296 -
2.297 class OutgoingMessage (Message):
2.298 "Abstract superclass of outgoing requests/responses."
2.299
2.300 - def __init__(self, connection, properties=None, body=None):
2.301 - Message.__init__(self,connection,properties,body)
2.302 + def __init__(self, connection, body=None, properties=None):
2.303 + Message.__init__(self,connection,body,properties)
2.304 self.urgent = self.compressed = self.noReply = False
2.305 - self.moreComing = True
2.306 + self._moreComing = True
2.307
2.308 def __setitem__(self, key,val):
2.309 self.properties[key] = val
2.310 def __delitem__(self, key):
2.311 del self.properties[key]
2.312
2.313 - def send(self):
2.314 - "Sends this message."
2.315 - log.info("Sending %s",self)
2.316 + @property
2.317 + def sent(self):
2.318 + return 'encoded' in self.__dict__
2.319 +
2.320 + def _encode(self):
2.321 + "Generates the message's encoded form, prior to sending it."
2.322 out = StringIO()
2.323 for (key,value) in self.properties.iteritems():
2.324 - def _writePropString(str):
2.325 - out.write(str) #FIX: Abbreviate
2.326 + def _writePropString(s):
2.327 + out.write(str(s)) #FIX: Abbreviate
2.328 out.write('\000')
2.329 _writePropString(key)
2.330 _writePropString(value)
2.331 - self.encoded = struct.pack('!H',out.tell()) + out.getvalue()
2.332 - out.close()
2.333 + propertiesSize = out.tell()
2.334 + assert propertiesSize<65536 #FIX: Return an error instead
2.335
2.336 body = self.body
2.337 if self.compressed:
2.338 - body = zlib.compress(body,5)
2.339 - self.encoded += body
2.340 + z = zlib.compressobj(6,zlib.DEFLATED,31) # window size of 31 needed for gzip format
2.341 + out.write(z.compress(body))
2.342 + body = z.flush()
2.343 + out.write(body)
2.344 +
2.345 + self.encoded = struct.pack('!H',propertiesSize) + out.getvalue()
2.346 + out.close()
2.347 log.debug("Encoded %s into %u bytes", self,len(self.encoded))
2.348 -
2.349 self.bytesSent = 0
2.350 - self.connection._outQueueMessage(self)
2.351
2.352 def _sendNextFrame(self, conn,maxLen):
2.353 pos = self.bytesSent
2.354 payload = self.encoded[pos:pos+maxLen]
2.355 pos += len(payload)
2.356 - self.moreComing = (pos < len(self.encoded))
2.357 + self._moreComing = (pos < len(self.encoded))
2.358 + if not self._moreComing:
2.359 + self.encoded = None
2.360 log.debug("Sending frame of %s; bytes %i--%i", self,pos-len(payload),pos)
2.361
2.362 conn.push( struct.pack(kFrameHeaderFormat, kFrameMagicNumber,
2.363 @@ -377,13 +456,15 @@
2.364 conn.push( payload )
2.365
2.366 self.bytesSent = pos
2.367 - return self.moreComing
2.368 + return self._moreComing
2.369
2.370
2.371 class Request (object):
2.372 @property
2.373 def response(self):
2.374 "The response object for this request."
2.375 + if self.noReply:
2.376 + return None
2.377 r = self.__dict__.get('_response')
2.378 if r==None:
2.379 r = self._response = self._createResponse()
2.380 @@ -391,7 +472,7 @@
2.381
2.382
2.383 class Response (Message):
2.384 - def __init__(self, request):
2.385 + def _setRequest(self, request):
2.386 assert not request.noReply
2.387 self.request = request
2.388 self.requestNo = request.requestNo
2.389 @@ -402,19 +483,24 @@
2.390 return True
2.391
2.392
2.393 -
2.394 class IncomingRequest (IncomingMessage, Request):
2.395 def _createResponse(self):
2.396 return OutgoingResponse(self)
2.397
2.398 +
2.399 class OutgoingRequest (OutgoingMessage, Request):
2.400 def _createResponse(self):
2.401 return IncomingResponse(self)
2.402 +
2.403 + def send(self):
2.404 + self._encode()
2.405 + return self.connection._sendRequest(self) and self.response
2.406 +
2.407
2.408 class IncomingResponse (IncomingMessage, Response):
2.409 def __init__(self, request):
2.410 - IncomingMessage.__init__(self,request.connection,request.requestNo,0)
2.411 - Response.__init__(self,request)
2.412 + IncomingMessage.__init__(self,request.connection,None,0)
2.413 + self._setRequest(request)
2.414 self.onComplete = None
2.415
2.416 def _finished(self):
2.417 @@ -424,40 +510,36 @@
2.418 self.onComplete(self)
2.419 except Exception, x:
2.420 log.error("Exception dispatching response: %s", traceback.format_exc())
2.421 -
2.422 +
2.423 +
2.424 class OutgoingResponse (OutgoingMessage, Response):
2.425 def __init__(self, request):
2.426 OutgoingMessage.__init__(self,request.connection)
2.427 - Response.__init__(self,request)
2.428 + self._setRequest(request)
2.429 +
2.430 + def send(self):
2.431 + self._encode()
2.432 + return self.connection._sendMessage(self)
2.433
2.434
2.435 -### UNIT TESTS:
2.436 -
2.437 -
2.438 -class BLIPTests(unittest.TestCase):
2.439 - def setUp(self):
2.440 - def handleRequest(request):
2.441 - logging.info("Got request!: %r",request)
2.442 - body = request.body
2.443 - assert len(body)<32768
2.444 - assert request.contentType == 'application/octet-stream'
2.445 - assert int(request['Size']) == len(body)
2.446 - assert request['User-Agent'] == 'BLIPConnectionTester'
2.447 - for i in xrange(0,len(request.body)):
2.448 - assert ord(body[i]) == i%256
2.449 -
2.450 - response = request.response
2.451 - response.body = request.body
2.452 - response['Content-Type'] = request.contentType
2.453 - response.send()
2.454 -
2.455 - listener = Listener(46353)
2.456 - listener.onRequest = handleRequest
2.457 -
2.458 - def testListener(self):
2.459 - logging.info("Waiting...")
2.460 - asyncore.loop()
2.461 -
2.462 -if __name__ == '__main__':
2.463 - logging.basicConfig(level=logging.INFO)
2.464 - unittest.main()
2.465 \ No newline at end of file
2.466 +"""
2.467 + Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
2.468 +
2.469 + Redistribution and use in source and binary forms, with or without modification, are permitted
2.470 + provided that the following conditions are met:
2.471 +
2.472 + * Redistributions of source code must retain the above copyright notice, this list of conditions
2.473 + and the following disclaimer.
2.474 + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
2.475 + and the following disclaimer in the documentation and/or other materials provided with the
2.476 + distribution.
2.477 +
2.478 + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
2.479 + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
2.480 + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
2.481 + BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
2.482 + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
2.483 + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
2.484 + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
2.485 + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2.486 +"""
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/Python/BLIPConnectionTest.py Wed Jun 04 17:11:20 2008 -0700
3.3 @@ -0,0 +1,72 @@
3.4 +#!/usr/bin/env python
3.5 +# encoding: utf-8
3.6 +"""
3.7 +BLIPConnectionTest.py
3.8 +
3.9 +Created by Jens Alfke on 2008-06-04.
3.10 +This source file is test/example code, and is in the public domain.
3.11 +"""
3.12 +
3.13 +from BLIP import Connection, OutgoingRequest, kOpening
3.14 +
3.15 +import asyncore
3.16 +from cStringIO import StringIO
3.17 +from datetime import datetime
3.18 +import logging
3.19 +import random
3.20 +import unittest
3.21 +
3.22 +
3.23 +kSendInterval = 2.0
3.24 +
3.25 +def randbool():
3.26 + return random.randint(0,1) == 1
3.27 +
3.28 +
3.29 +class BLIPConnectionTest(unittest.TestCase):
3.30 +
3.31 + def setUp(self):
3.32 + self.connection = Connection( ('localhost',46353) )
3.33 +
3.34 + def sendRequest(self):
3.35 + size = random.randint(0,32767)
3.36 + io = StringIO()
3.37 + for i in xrange(0,size):
3.38 + io.write( chr(i % 256) )
3.39 + body = io.getvalue()
3.40 + io.close
3.41 +
3.42 + req = OutgoingRequest(self.connection, body,{'Content-Type': 'application/octet-stream',
3.43 + 'User-Agent': 'PyBLIP',
3.44 + 'Date': datetime.now(),
3.45 + 'Size': size})
3.46 + req.compressed = randbool()
3.47 + req.urgent = randbool()
3.48 + req.response.onComplete = self.gotResponse
3.49 + return req.send()
3.50 +
3.51 + def gotResponse(self, response):
3.52 + logging.info("Got response!: %s",response)
3.53 + request = response.request
3.54 + assert response.body == request.body
3.55 +
3.56 + def testClient(self):
3.57 + lastReqTime = None
3.58 + nRequests = 0
3.59 + while nRequests < 10:
3.60 + asyncore.loop(timeout=kSendInterval,count=1)
3.61 +
3.62 + now = datetime.now()
3.63 + if self.connection.status!=kOpening and not lastReqTime or (now-lastReqTime).seconds >= kSendInterval:
3.64 + lastReqTime = now
3.65 + if not self.sendRequest():
3.66 + logging.warn("Couldn't send request (connection is probably closed)")
3.67 + break;
3.68 + nRequests += 1
3.69 +
3.70 + def tearDown(self):
3.71 + self.connection.close()
3.72 +
3.73 +if __name__ == '__main__':
3.74 + logging.basicConfig(level=logging.INFO)
3.75 + unittest.main()
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/Python/BLIPListenerTest.py Wed Jun 04 17:11:20 2008 -0700
4.3 @@ -0,0 +1,46 @@
4.4 +#!/usr/bin/env python
4.5 +# encoding: utf-8
4.6 +"""
4.7 +BLIPListenerTest.py
4.8 +
4.9 +Created by Jens Alfke on 2008-06-04.
4.10 +This source file is test/example code, and is in the public domain.
4.11 +"""
4.12 +
4.13 +from BLIP import Listener
4.14 +
4.15 +import asyncore
4.16 +import logging
4.17 +import unittest
4.18 +
4.19 +
4.20 +class BLIPListenerTest(unittest.TestCase):
4.21 +
4.22 + def testListener(self):
4.23 + def handleRequest(request):
4.24 + logging.info("Got request!: %r",request)
4.25 + body = request.body
4.26 + assert len(body)<32768
4.27 + assert request.contentType == 'application/octet-stream'
4.28 + assert int(request['Size']) == len(body)
4.29 + assert request['User-Agent'] != None
4.30 + for i in xrange(0,len(request.body)):
4.31 + assert ord(body[i]) == i%256
4.32 +
4.33 + response = request.response
4.34 + response.body = request.body
4.35 + response['Content-Type'] = request.contentType
4.36 + response.send()
4.37 +
4.38 + listener = Listener(46353)
4.39 + listener.onRequest = handleRequest
4.40 + logging.info("Listener is waiting...")
4.41 +
4.42 + try:
4.43 + asyncore.loop()
4.44 + except KeyboardInterrupt:
4.45 + logging.info("KeyboardInterrupt")
4.46 +
4.47 +if __name__ == '__main__':
4.48 + logging.basicConfig(level=logging.INFO)
4.49 + unittest.main()