BROKEN COMMIT. Majority of code to handle closing has been added. Listeners do not close correctly.
authormorrowa@betelgeuse.local
Tue Jun 23 11:44:30 2009 -0700 (2009-06-23)
changeset 51de59ce19f42e
parent 49 20cccc7c26ee
child 52 d2e6fb7192ac
BROKEN COMMIT. Majority of code to handle closing has been added. Listeners do not close correctly.
1_0_to_1_1_diffs.diff
Python/BLIP.py
Python/BLIPConnectionTest.py
Python/BLIPListenerTest.py
Python/CloseTestPing.py
Python/CloseTestPong.py
Python/asynchatPing.py
Python/asynchatPong.py
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/1_0_to_1_1_diffs.diff	Tue Jun 23 11:44:30 2009 -0700
     1.3 @@ -0,0 +1,425 @@
     1.4 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIPConnection.h
     1.5 +--- a/BLIP/BLIPConnection.h	Thu Jun 19 10:22:19 2008 -0700
     1.6 ++++ b/BLIP/BLIPConnection.h	Mon Jun 23 14:02:31 2008 -0700
     1.7 +@@ -20,6 +20,7 @@
     1.8 + @interface BLIPConnection : TCPConnection
     1.9 + {
    1.10 +     BLIPDispatcher *_dispatcher;
    1.11 ++    BOOL _blipClosing;
    1.12 + }
    1.13 + 
    1.14 + /** The delegate object that will be called when the connection opens, closes or receives messages. */
    1.15 +@@ -73,6 +74,13 @@
    1.16 + /** Called when a BLIPResponse (to one of your requests) is received from the peer.
    1.17 +     This is called <i>after</i> the response object's onComplete target, if any, is invoked.*/
    1.18 + - (void) connection: (BLIPConnection*)connection receivedResponse: (BLIPResponse*)response;
    1.19 ++
    1.20 ++/** Called when the peer wants to close the connection. Return YES to allow, NO to prevent. */
    1.21 ++- (BOOL) connectionReceivedCloseRequest: (BLIPConnection*)connection;
    1.22 ++
    1.23 ++/** Called if the peer refuses a close request. 
    1.24 ++    The typical error is kBLIPError_Forbidden. */
    1.25 ++- (void) connection: (BLIPConnection*)connection closeRequestFailedWithError: (NSError*)error;
    1.26 + @end
    1.27 + 
    1.28 + 
    1.29 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIPConnection.m
    1.30 +--- a/BLIP/BLIPConnection.m	Thu Jun 19 10:22:19 2008 -0700
    1.31 ++++ b/BLIP/BLIPConnection.m	Mon Jun 23 14:02:31 2008 -0700
    1.32 +@@ -8,6 +8,7 @@
    1.33 + 
    1.34 + #import "BLIPConnection.h"
    1.35 + #import "BLIP_Internal.h"
    1.36 ++#import "TCP_Internal.h"
    1.37 + #import "BLIPReader.h"
    1.38 + #import "BLIPWriter.h"
    1.39 + #import "BLIPDispatcher.h"
    1.40 +@@ -15,6 +16,7 @@
    1.41 + #import "Logging.h"
    1.42 + #import "Test.h"
    1.43 + #import "ExceptionUtils.h"
    1.44 ++#import "Target.h"
    1.45 + 
    1.46 + 
    1.47 + NSString* const BLIPErrorDomain = @"BLIP";
    1.48 +@@ -33,10 +35,14 @@
    1.49 + }
    1.50 + 
    1.51 + 
    1.52 ++@interface BLIPConnection ()
    1.53 ++- (void) _handleCloseRequest: (BLIPRequest*)request;
    1.54 ++@end
    1.55 + 
    1.56 + 
    1.57 + @implementation BLIPConnection
    1.58 + 
    1.59 ++
    1.60 + - (void) dealloc
    1.61 + {
    1.62 +     [_dispatcher release];
    1.63 +@@ -48,6 +54,11 @@
    1.64 + - (id<BLIPConnectionDelegate>) delegate                     {return (id)_delegate;}
    1.65 + - (void) setDelegate: (id<BLIPConnectionDelegate>)delegate  {_delegate = delegate;}
    1.66 + 
    1.67 ++
    1.68 ++#pragma mark -
    1.69 ++#pragma mark RECEIVING:
    1.70 ++
    1.71 ++
    1.72 + - (BLIPDispatcher*) dispatcher
    1.73 + {
    1.74 +     if( ! _dispatcher ) {
    1.75 +@@ -58,11 +69,23 @@
    1.76 + }
    1.77 + 
    1.78 + 
    1.79 ++- (void) _dispatchMetaRequest: (BLIPRequest*)request
    1.80 ++{
    1.81 ++    NSString* profile = request.profile;
    1.82 ++    if( [profile isEqualToString: kBLIPProfile_Bye] )
    1.83 ++        [self _handleCloseRequest: request];
    1.84 ++    else
    1.85 ++        [request respondWithErrorCode: kBLIPError_NotFound message: @"Unknown meta profile"];
    1.86 ++}
    1.87 ++
    1.88 ++
    1.89 + - (void) _dispatchRequest: (BLIPRequest*)request
    1.90 + {
    1.91 +     LogTo(BLIP,@"Received all of %@",request.descriptionWithProperties);
    1.92 +     @try{
    1.93 +-        if( ! [self.dispatcher dispatchMessage: request] )
    1.94 ++        if( request._flags & kBLIP_Meta )
    1.95 ++            [self _dispatchMetaRequest: request];
    1.96 ++        else if( ! [self.dispatcher dispatchMessage: request] )
    1.97 +             [self tellDelegate: @selector(connection:receivedRequest:) withObject: request];
    1.98 +         if( ! request.noReply && ! request.repliedTo ) {
    1.99 +             LogTo(BLIP,@"Returning default empty response to %@",request);
   1.100 +@@ -81,6 +104,10 @@
   1.101 + }
   1.102 + 
   1.103 + 
   1.104 ++#pragma mark -
   1.105 ++#pragma mark SENDING:
   1.106 ++
   1.107 ++
   1.108 + - (BLIPRequest*) request
   1.109 + {
   1.110 +     return [[[BLIPRequest alloc] _initWithConnection: self body: nil properties: nil] autorelease];
   1.111 +@@ -103,11 +130,61 @@
   1.112 + }
   1.113 + 
   1.114 + 
   1.115 ++#pragma mark -
   1.116 ++#pragma mark CLOSING:
   1.117 ++
   1.118 ++
   1.119 ++- (void) _beginClose
   1.120 ++{
   1.121 ++    // Override of TCPConnection method. Instead of closing the socket, send a 'bye' request:
   1.122 ++    if( ! _blipClosing ) {
   1.123 ++        LogTo(BLIPVerbose,@"Sending close request...");
   1.124 ++        BLIPRequest *r = [self request];
   1.125 ++        [r _setFlag: kBLIP_Meta value: YES];
   1.126 ++        r.profile = kBLIPProfile_Bye;
   1.127 ++        BLIPResponse *response = [r send];
   1.128 ++        response.onComplete = $target(self,_receivedCloseResponse:);
   1.129 ++    }
   1.130 ++    // Put the writer in close mode, to prevent client from sending any more requests:
   1.131 ++    [self.writer close];
   1.132 ++}
   1.133 ++
   1.134 ++- (void) _receivedCloseResponse: (BLIPResponse*)response
   1.135 ++{
   1.136 ++    NSError *error = response.error;
   1.137 ++    LogTo(BLIPVerbose,@"Received close response: error=%@",error);
   1.138 ++    if( error ) {
   1.139 ++        [self _unclose];
   1.140 ++        [self tellDelegate: @selector(connection:closeRequestFailedWithError:) withObject: error];
   1.141 ++    } else {
   1.142 ++        // Now finally close the socket:
   1.143 ++        [super _beginClose];
   1.144 ++    }
   1.145 ++}
   1.146 ++
   1.147 ++
   1.148 ++- (void) _handleCloseRequest: (BLIPRequest*)request
   1.149 ++{
   1.150 ++    LogTo(BLIPVerbose,@"Received a close request");
   1.151 ++    if( [_delegate respondsToSelector: @selector(connectionReceivedCloseRequest:)] )
   1.152 ++        if( ! [_delegate connectionReceivedCloseRequest: self] ) {
   1.153 ++            LogTo(BLIPVerbose,@"Responding with denial of close request");
   1.154 ++            [request respondWithErrorCode: kBLIPError_Forbidden message: @"Close request denied"];
   1.155 ++            return;
   1.156 ++        }
   1.157 ++    
   1.158 ++    LogTo(BLIPVerbose,@"Close request accepted");
   1.159 ++    _blipClosing = YES; // this prevents _beginClose from sending a close request back
   1.160 ++    [self close];
   1.161 ++}
   1.162 ++
   1.163 ++
   1.164 + @end
   1.165 + 
   1.166 + 
   1.167 + 
   1.168 + 
   1.169 ++#pragma mark -
   1.170 + @implementation BLIPListener
   1.171 + 
   1.172 + - (id) initWithPort: (UInt16)port
   1.173 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIPMessage.m
   1.174 +--- a/BLIP/BLIPMessage.m	Thu Jun 19 10:22:19 2008 -0700
   1.175 ++++ b/BLIP/BLIPMessage.m	Mon Jun 23 14:02:31 2008 -0700
   1.176 +@@ -74,6 +74,8 @@
   1.177 +         [desc appendString: @", urgent"];
   1.178 +     if( _flags & kBLIP_NoReply )
   1.179 +         [desc appendString: @", noreply"];
   1.180 ++    if( _flags & kBLIP_Meta )
   1.181 ++        [desc appendString: @", META"];
   1.182 +     [desc appendString: @"]"];
   1.183 +     return desc;
   1.184 + }
   1.185 +@@ -103,6 +105,8 @@
   1.186 +         _flags &= ~flag;
   1.187 + }
   1.188 + 
   1.189 ++- (BLIPMessageFlags) _flags                 {return _flags;}
   1.190 ++
   1.191 + - (BOOL) compressed                         {return (_flags & kBLIP_Compressed) != 0;}
   1.192 + - (BOOL) urgent                             {return (_flags & kBLIP_Urgent) != 0;}
   1.193 + - (void) setCompressed: (BOOL)compressed    {[self _setFlag: kBLIP_Compressed value: compressed];}
   1.194 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIPReader.m
   1.195 +--- a/BLIP/BLIPReader.m	Thu Jun 19 10:22:19 2008 -0700
   1.196 ++++ b/BLIP/BLIPReader.m	Mon Jun 23 14:02:31 2008 -0700
   1.197 +@@ -93,7 +93,7 @@
   1.198 + 
   1.199 + - (BOOL) isBusy
   1.200 + {
   1.201 +-    return _curBytesRead > 0;
   1.202 ++    return _curBytesRead > 0 || _pendingRequests.count > 0 || _pendingResponses.count > 0;
   1.203 + }
   1.204 + 
   1.205 + 
   1.206 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIPRequest.m
   1.207 +--- a/BLIP/BLIPRequest.m	Thu Jun 19 10:22:19 2008 -0700
   1.208 ++++ b/BLIP/BLIPRequest.m	Mon Jun 23 14:02:31 2008 -0700
   1.209 +@@ -199,6 +199,8 @@
   1.210 +         setObj(&_mutableBody,nil);
   1.211 +         
   1.212 +         BLIPMutableProperties *errorProps = [self.properties mutableCopy];
   1.213 ++        if( ! errorProps )
   1.214 ++            errorProps = [[BLIPMutableProperties alloc] init];
   1.215 +         NSDictionary *userInfo = error.userInfo;
   1.216 +         for( NSString *key in userInfo ) {
   1.217 +             id value = $castIf(NSString,[userInfo objectForKey: key]);
   1.218 +@@ -227,8 +229,12 @@
   1.219 + {
   1.220 +     Assert(_connection,@"%@ has no connection to send over",self);
   1.221 +     Assert(!_sent,@"%@ was already sent",self);
   1.222 ++    BLIPWriter *writer = (BLIPWriter*)_connection.writer;
   1.223 ++    Assert(writer,@"%@'s connection has no writer (already closed?)",self);
   1.224 +     [self _encode];
   1.225 +-    return (self.sent = [(BLIPWriter*)_connection.writer sendMessage: self]);
   1.226 ++    BOOL sent = self.sent = [writer sendMessage: self];
   1.227 ++    Assert(sent);
   1.228 ++    return sent;
   1.229 + }
   1.230 + 
   1.231 + 
   1.232 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIPTest.m
   1.233 +--- a/BLIP/BLIPTest.m	Thu Jun 19 10:22:19 2008 -0700
   1.234 ++++ b/BLIP/BLIPTest.m	Mon Jun 23 14:02:31 2008 -0700
   1.235 +@@ -35,6 +35,7 @@
   1.236 + #define kClientUsesSSLCert          NO
   1.237 + #define kListenerRequiresSSL        NO
   1.238 + #define kListenerRequiresClientCert NO
   1.239 ++#define kListenerCloseAfter         50
   1.240 + 
   1.241 + 
   1.242 + static SecIdentityRef GetClientIdentity(void) {
   1.243 +@@ -100,36 +101,38 @@
   1.244 + 
   1.245 + - (void) sendAMessage
   1.246 + {
   1.247 +-    if(_pending.count<100) {
   1.248 +-        Log(@"** Sending another %i messages...", kNBatchedMessages);
   1.249 +-        for( int i=0; i<kNBatchedMessages; i++ ) {
   1.250 +-            size_t size = random() % 32768;
   1.251 +-            NSMutableData *body = [NSMutableData dataWithLength: size];
   1.252 +-            UInt8 *bytes = body.mutableBytes;
   1.253 +-            for( size_t i=0; i<size; i++ )
   1.254 +-                bytes[i] = i % 256;
   1.255 +-            
   1.256 +-            BLIPRequest *q = [_conn requestWithBody: body
   1.257 +-                                         properties: $dict({@"Content-Type", @"application/octet-stream"},
   1.258 +-                                                           {@"User-Agent", @"BLIPConnectionTester"},
   1.259 +-                                                           {@"Date", [[NSDate date] description]},
   1.260 +-                                                           {@"Size",$sprintf(@"%u",size)})];
   1.261 +-            Assert(q);
   1.262 +-            if( kUseCompression && (random()%2==1) )
   1.263 +-                q.compressed = YES;
   1.264 +-            if( random()%16 > 12 )
   1.265 +-                q.urgent = YES;
   1.266 +-            BLIPResponse *response = [q send];
   1.267 +-            Assert(response);
   1.268 +-            Assert(q.number>0);
   1.269 +-            Assert(response.number==q.number);
   1.270 +-            [_pending setObject: $object(size) forKey: $object(q.number)];
   1.271 +-            response.onComplete = $target(self,responseArrived:);
   1.272 ++    if( _conn.status==kTCP_Open || _conn.status==kTCP_Opening ) {
   1.273 ++        if(_pending.count<100) {
   1.274 ++            Log(@"** Sending another %i messages...", kNBatchedMessages);
   1.275 ++            for( int i=0; i<kNBatchedMessages; i++ ) {
   1.276 ++                size_t size = random() % 32768;
   1.277 ++                NSMutableData *body = [NSMutableData dataWithLength: size];
   1.278 ++                UInt8 *bytes = body.mutableBytes;
   1.279 ++                for( size_t i=0; i<size; i++ )
   1.280 ++                    bytes[i] = i % 256;
   1.281 ++                
   1.282 ++                BLIPRequest *q = [_conn requestWithBody: body
   1.283 ++                                             properties: $dict({@"Content-Type", @"application/octet-stream"},
   1.284 ++                                                               {@"User-Agent", @"BLIPConnectionTester"},
   1.285 ++                                                               {@"Date", [[NSDate date] description]},
   1.286 ++                                                               {@"Size",$sprintf(@"%u",size)})];
   1.287 ++                Assert(q);
   1.288 ++                if( kUseCompression && (random()%2==1) )
   1.289 ++                    q.compressed = YES;
   1.290 ++                if( random()%16 > 12 )
   1.291 ++                    q.urgent = YES;
   1.292 ++                BLIPResponse *response = [q send];
   1.293 ++                Assert(response);
   1.294 ++                Assert(q.number>0);
   1.295 ++                Assert(response.number==q.number);
   1.296 ++                [_pending setObject: $object(size) forKey: $object(q.number)];
   1.297 ++                response.onComplete = $target(self,responseArrived:);
   1.298 ++            }
   1.299 ++        } else {
   1.300 ++            Warn(@"There are %u pending messages; waiting for the listener to catch up...",_pending.count);
   1.301 +         }
   1.302 +-    } else {
   1.303 +-        Warn(@"There are %u pending messages; waiting for the listener to catch up...",_pending.count);
   1.304 ++        [self performSelector: @selector(sendAMessage) withObject: nil afterDelay: kSendInterval];
   1.305 +     }
   1.306 +-    [self performSelector: @selector(sendAMessage) withObject: nil afterDelay: kSendInterval];
   1.307 + }
   1.308 + 
   1.309 + - (void) responseArrived: (BLIPResponse*)response
   1.310 +@@ -191,6 +194,13 @@
   1.311 +     Log(@"Now %u replies pending", _pending.count);
   1.312 + }
   1.313 + 
   1.314 ++- (BOOL) connectionReceivedCloseRequest: (BLIPConnection*)connection
   1.315 ++{
   1.316 ++    BOOL response = NO;
   1.317 ++    Log(@"***** %@ received a close request; returning %i",connection,response);
   1.318 ++    return response;
   1.319 ++}
   1.320 ++
   1.321 + 
   1.322 + @end
   1.323 + 
   1.324 +@@ -217,6 +227,7 @@
   1.325 + @interface BLIPTestListener : NSObject <TCPListenerDelegate, BLIPConnectionDelegate>
   1.326 + {
   1.327 +     BLIPListener *_listener;
   1.328 ++    int _nReceived;
   1.329 + }
   1.330 + 
   1.331 + @end
   1.332 +@@ -277,6 +288,7 @@
   1.333 + - (void) connectionDidOpen: (TCPConnection*)connection
   1.334 + {
   1.335 +     Log(@"** %@ didOpen [SSL=%@]",connection,connection.actualSecurityLevel);
   1.336 ++    _nReceived = 0;
   1.337 + }
   1.338 + - (BOOL) connection: (TCPConnection*)connection authorizeSSLPeer: (SecCertificateRef)peerCert
   1.339 + {
   1.340 +@@ -312,6 +324,22 @@
   1.341 +     AssertEq([[request valueOfProperty: @"Size"] intValue], size);
   1.342 + 
   1.343 +     [request respondWithData: body contentType: request.contentType];
   1.344 ++    
   1.345 ++    if( ++ _nReceived == kListenerCloseAfter ) {
   1.346 ++        Log(@"********** Closing BLIPTestListener after %i requests",_nReceived);
   1.347 ++        [connection close];
   1.348 ++    }
   1.349 ++}
   1.350 ++
   1.351 ++- (BOOL) connectionReceivedCloseRequest: (BLIPConnection*)connection;
   1.352 ++{
   1.353 ++    Log(@"***** %@ received a close request",connection);
   1.354 ++    return YES;
   1.355 ++}
   1.356 ++
   1.357 ++- (void) connection: (BLIPConnection*)connection closeRequestFailedWithError: (NSError*)error
   1.358 ++{
   1.359 ++    Log(@"***** %@'s close request failed: %@",connection,error);
   1.360 + }
   1.361 + 
   1.362 + 
   1.363 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIPWriter.m
   1.364 +--- a/BLIP/BLIPWriter.m	Thu Jun 19 10:22:19 2008 -0700
   1.365 ++++ b/BLIP/BLIPWriter.m	Mon Jun 23 14:02:31 2008 -0700
   1.366 +@@ -79,10 +79,6 @@
   1.367 + 
   1.368 + - (BOOL) sendMessage: (BLIPMessage*)message
   1.369 + {
   1.370 +-    if( _shouldClose ) {
   1.371 +-        Warn(@"%@: Attempt to send a message after the connection has started closing",self);
   1.372 +-        return NO;
   1.373 +-    }
   1.374 +     Assert(!message.sent,@"message has already been sent");
   1.375 +     [self _queueMessage: message isNew: YES];
   1.376 +     return YES;
   1.377 +@@ -91,12 +87,14 @@
   1.378 + 
   1.379 + - (BOOL) sendRequest: (BLIPRequest*)q response: (BLIPResponse*)response
   1.380 + {
   1.381 +-    if( !_shouldClose ) {
   1.382 +-        [q _assignedNumber: ++_numRequestsSent];
   1.383 +-        if( response ) {
   1.384 +-            [response _assignedNumber: _numRequestsSent];
   1.385 +-            [(BLIPReader*)self.reader _addPendingResponse: response];
   1.386 +-        }
   1.387 ++    if( _shouldClose ) {
   1.388 ++        Warn(@"%@: Attempt to send a request after the connection has started closing: %@",self,q);
   1.389 ++        return NO;
   1.390 ++    }
   1.391 ++    [q _assignedNumber: ++_numRequestsSent];
   1.392 ++    if( response ) {
   1.393 ++        [response _assignedNumber: _numRequestsSent];
   1.394 ++        [(BLIPReader*)self.reader _addPendingResponse: response];
   1.395 +     }
   1.396 +     return [self sendMessage: q];
   1.397 + }
   1.398 +diff -r 70590cc555aa -r 16454d63d4c2 BLIP/BLIP_Internal.h
   1.399 +--- a/BLIP/BLIP_Internal.h	Thu Jun 19 10:22:19 2008 -0700
   1.400 ++++ b/BLIP/BLIP_Internal.h	Mon Jun 23 14:02:31 2008 -0700
   1.401 +@@ -29,6 +29,7 @@
   1.402 +     kBLIP_Urgent    = 0x0020,       // please send sooner/faster
   1.403 +     kBLIP_NoReply   = 0x0040,       // no RPY needed
   1.404 +     kBLIP_MoreComing= 0x0080,       // More frames coming (Applies only to individual frame)
   1.405 ++    kBLIP_Meta      = 0x0100,       // Special message type, handled internally (hello, bye, ...)
   1.406 + };
   1.407 + typedef UInt16 BLIPMessageFlags;
   1.408 + 
   1.409 +@@ -41,7 +42,10 @@
   1.410 +     UInt16           size;          // total size of frame, _including_ this header
   1.411 + } BLIPFrameHeader;
   1.412 + 
   1.413 +-#define kBLIPFrameHeaderMagicNumber 0x9B34F205
   1.414 ++#define kBLIPFrameHeaderMagicNumber 0x9B34F206
   1.415 ++
   1.416 ++#define kBLIPProfile_Hi  @"Hi"      // Used for Profile header in meta greeting message
   1.417 ++#define kBLIPProfile_Bye @"Bye"     // Used for Profile header in meta close-request message
   1.418 + 
   1.419 + 
   1.420 + @interface BLIPConnection ()
   1.421 +@@ -52,6 +56,7 @@
   1.422 + 
   1.423 + @interface BLIPMessage ()
   1.424 + @property BOOL sent, propertiesAvailable, complete;
   1.425 ++- (BLIPMessageFlags) _flags;
   1.426 + - (void) _setFlag: (BLIPMessageFlags)flag value: (BOOL)value;
   1.427 + - (void) _encode;
   1.428 + @end
     2.1 --- a/Python/BLIP.py	Sun May 24 15:03:39 2009 -0700
     2.2 +++ b/Python/BLIP.py	Tue Jun 23 11:44:30 2009 -0700
     2.3 @@ -27,7 +27,7 @@
     2.4  
     2.5  # INTERNAL CONSTANTS -- NO TOUCHIES!
     2.6  
     2.7 -kFrameMagicNumber   = 0x9B34F205
     2.8 +kFrameMagicNumber   = 0x9B34F206
     2.9  kFrameHeaderFormat  = '!LLHH'
    2.10  kFrameHeaderSize    = 12
    2.11  
    2.12 @@ -36,11 +36,15 @@
    2.13  kMsgFlag_Urgent     = 0x0020
    2.14  kMsgFlag_NoReply    = 0x0040
    2.15  kMsgFlag_MoreComing = 0x0080
    2.16 +kMsgFlag_Meta       = 0x0100
    2.17  
    2.18  kMsgType_Request    = 0
    2.19  kMsgType_Response   = 1
    2.20  kMsgType_Error      = 2
    2.21  
    2.22 +kMsgProfile_Hi      = "Hi"
    2.23 +kMsgProfile_Bye     = "Bye"
    2.24 +
    2.25  
    2.26  log = logging.getLogger('BLIP')
    2.27  log.propagate = True
    2.28 @@ -104,7 +108,7 @@
    2.29              self.connect(address)
    2.30          self.address = address
    2.31          self.listener = listener
    2.32 -        self.onRequest = None
    2.33 +        self.onRequest = self.onCloseRequest = self.onCloseRefused = None
    2.34          self.pendingRequests = {}
    2.35          self.pendingResponses = {}
    2.36          self.outBox = []
    2.37 @@ -112,12 +116,7 @@
    2.38          self.inNumRequests = self.outNumRequests = 0
    2.39          self.sending = False
    2.40          self._endOfFrame()
    2.41 -    
    2.42 -    def close(self):
    2.43 -        if self.status > kClosed:
    2.44 -            self.status = kClosing
    2.45 -            log.info("Connection closing...")
    2.46 -        asynchat.async_chat.close(self)
    2.47 +        self._closeWhenPossible = False
    2.48      
    2.49      def handle_connect(self):
    2.50          log.info("Connection open!")
    2.51 @@ -130,25 +129,19 @@
    2.52          self.status = kDisconnected
    2.53          self.close()
    2.54      
    2.55 -    def handle_close(self):
    2.56 -        log.info("Connection closed!")
    2.57 -        self.pendingRequests = self.pendingResponses = None
    2.58 -        self.outBox = None
    2.59 -        if self.status == kClosing:
    2.60 -            self.status = kClosed
    2.61 -        else:
    2.62 -            self.status = kDisconnected
    2.63 -        asynchat.async_chat.handle_close(self)
    2.64 -        
    2.65      
    2.66      ### SENDING:
    2.67      
    2.68      @property
    2.69 -    def canSend(self):
    2.70 +    def isOpen(self):
    2.71          return self.status==kOpening or self.status==kOpen
    2.72      
    2.73 +    @property
    2.74 +    def canSend(self):
    2.75 +        return self.isOpen and not self._closeWhenPossible
    2.76 +    
    2.77      def _sendMessage(self, msg):
    2.78 -        if self.canSend:
    2.79 +        if self.isOpen:
    2.80              self._outQueueMessage(msg,True)
    2.81              if not self.sending:
    2.82                  log.debug("Waking up the output stream")
    2.83 @@ -208,6 +201,7 @@
    2.84          else:
    2.85              log.debug("Nothing more to send")
    2.86              self.sending = False
    2.87 +            self._closeIfReady()
    2.88              return None
    2.89      
    2.90      ### RECEIVING:
    2.91 @@ -255,6 +249,7 @@
    2.92                  self.inNumRequests += 1
    2.93          elif msgType==kMsgType_Response or msgType==kMsgType_Error:
    2.94              message = self.pendingResponses.get(requestNo)
    2.95 +            message._updateFlags(flags)
    2.96          
    2.97          if message != None:
    2.98              message._beginFrame(flags)
    2.99 @@ -284,10 +279,88 @@
   2.100          try:
   2.101              msg._finished()
   2.102              if not msg.isResponse:
   2.103 -                self.onRequest(msg)
   2.104 +                if msg._meta:
   2.105 +                    self._dispatchMetaRequest(msg)
   2.106 +                else:
   2.107 +                    self.onRequest(msg)
   2.108          except Exception, x:
   2.109              log.error("Exception handling incoming message: %s", traceback.format_exc())
   2.110              #FIX: Send an error reply
   2.111 +        # Check to see if we're done and ready to close:
   2.112 +        self._closeIfReady()
   2.113 +    
   2.114 +    def _dispatchMetaRequest(self, request):
   2.115 +        """Handles dispatching internal meta requests."""
   2.116 +        if request['Profile'] == kMsgProfile_Bye:
   2.117 +            shouldClose = True
   2.118 +            if self.onCloseRequest:
   2.119 +                shouldClose = self.onCloseRequest()
   2.120 +            if not shouldClose:
   2.121 +                log.debug("Sending resfusal to close...")
   2.122 +                response = request.response
   2.123 +                response.isError = True
   2.124 +                response['Error-Domain'] = "BLIP"
   2.125 +                response['Error-Code'] = 403
   2.126 +                response.body = "Close request denied"
   2.127 +                response.send()
   2.128 +            else:
   2.129 +                log.debug("Sending permission to close...")
   2.130 +                response = request.response
   2.131 +                response.send()
   2.132 +        else:
   2.133 +            response = request.response
   2.134 +            response.isError = True
   2.135 +            response['Error-Domain'] = "BLIP"
   2.136 +            response['Error-Code'] = 404
   2.137 +            response.body = "Unknown meta profile"
   2.138 +            response.send()
   2.139 +    
   2.140 +    ### CLOSING:
   2.141 +    
   2.142 +    def close(self):
   2.143 +        """Publicly callable close method. Sends close request to peer."""
   2.144 +        if self.status != kOpen:
   2.145 +            return False
   2.146 +        log.info("Sending close request...")
   2.147 +        req = OutgoingRequest(self, None, {'Profile': kMsgProfile_Bye})
   2.148 +        req._meta = True
   2.149 +        req.response.onComplete = self._handleCloseResponse
   2.150 +        if not req.send():
   2.151 +            log.error("Error sending close request.")
   2.152 +            return False
   2.153 +        else:
   2.154 +            self.status = kClosing
   2.155 +        return True
   2.156 +    
   2.157 +    def _handleCloseResponse(self, response):
   2.158 +        """Called when we receive a response to a close request."""
   2.159 +        log.info("Received close response.")
   2.160 +        if response.isError:
   2.161 +            # remote refused to close
   2.162 +            if self.onCloseRefused:
   2.163 +                self.onCloseRefused(response)
   2.164 +            self.status = kOpen
   2.165 +        else:
   2.166 +            # now wait until everything has finished sending, then actually close
   2.167 +            log.info("No refusal, actually closing...")
   2.168 +            self._closeWhenPossible = True
   2.169 +    
   2.170 +    def _closeIfReady(self):
   2.171 +        """Checks if all transmissions are complete and then closes the actual socket."""
   2.172 +        if self._closeWhenPossible and len(self.outBox) == 0 and len(self.pendingRequests) == 0 and len(self.pendingResponses) == 0:
   2.173 +            # self._closeWhenPossible = False
   2.174 +            log.debug("_closeIfReady closing.")
   2.175 +            asynchat.async_chat.close(self)
   2.176 +    
   2.177 +    def handle_close(self):
   2.178 +        """Called when the socket actually closes."""
   2.179 +        log.info("Connection closed!")
   2.180 +        self.pendingRequests = self.pendingResponses = None
   2.181 +        self.outBox = None
   2.182 +        if self.status == kClosing:
   2.183 +            self.status = kClosed
   2.184 +        else:
   2.185 +            self.status = kDisconnected
   2.186  
   2.187  
   2.188  ### MESSAGE CLASSES:
   2.189 @@ -305,13 +378,17 @@
   2.190      @property
   2.191      def flags(self):
   2.192          if self.isResponse:
   2.193 -            flags = kMsgType_Response
   2.194 +            if self.isError:
   2.195 +                flags = kMsgType_Error
   2.196 +            else:
   2.197 +                flags = kMsgType_Response
   2.198          else:
   2.199              flags = kMsgType_Request
   2.200          if self.urgent:     flags |= kMsgFlag_Urgent
   2.201          if self.compressed: flags |= kMsgFlag_Compressed
   2.202          if self.noReply:    flags |= kMsgFlag_NoReply
   2.203          if self._moreComing:flags |= kMsgFlag_MoreComing
   2.204 +        if self._meta:      flags |= kMsgFlag_Meta
   2.205          return flags
   2.206      
   2.207      def __str__(self):
   2.208 @@ -322,6 +399,7 @@
   2.209          if self.compressed: s += " CMP"
   2.210          if self.noReply:    s += " NOR"
   2.211          if self._moreComing:s += " MOR"
   2.212 +        if self._meta:      s += " MET"
   2.213          if self.body:       s += " %i bytes" %len(self.body)
   2.214          return s+"]"
   2.215      
   2.216 @@ -352,11 +430,16 @@
   2.217      def __init__(self, connection, requestNo, flags):
   2.218          super(IncomingMessage,self).__init__(connection)
   2.219          self.requestNo  = requestNo
   2.220 +        self._updateFlags(flags)
   2.221 +        self.frames     = []
   2.222 +    
   2.223 +    def _updateFlags(self, flags):
   2.224          self.urgent     = (flags & kMsgFlag_Urgent) != 0
   2.225          self.compressed = (flags & kMsgFlag_Compressed) != 0
   2.226          self.noReply    = (flags & kMsgFlag_NoReply) != 0
   2.227          self._moreComing= (flags & kMsgFlag_MoreComing) != 0
   2.228 -        self.frames     = []
   2.229 +        self._meta      = (flags & kMsgFlag_Meta) != 0
   2.230 +        self.isError    = (flags & kMsgType_Error) != 0
   2.231      
   2.232      def _beginFrame(self, flags):
   2.233          """Received a frame header."""
   2.234 @@ -377,16 +460,18 @@
   2.235          if propSize>len(encoded): raise MessageException, "properties too long to fit"
   2.236          if propSize>2 and encoded[propSize-1] != '\000': raise MessageException, "properties are not nul-terminated"
   2.237          
   2.238 -        proplist = encoded[2:propSize-1].split('\000')
   2.239 +        if propSize > 2:
   2.240 +            proplist = encoded[2:propSize-1].split('\000')
   2.241 +        
   2.242 +            if len(proplist) & 1: raise MessageException, "odd number of property strings"
   2.243 +            for i in xrange(0,len(proplist),2):
   2.244 +                def expand(str):
   2.245 +                    if len(str)==1:
   2.246 +                        str = IncomingMessage.__expandDict.get(str,str)
   2.247 +                    return str
   2.248 +                self.properties[ expand(proplist[i])] = expand(proplist[i+1])
   2.249 +        
   2.250          encoded = encoded[propSize:]
   2.251 -        if len(proplist) & 1: raise MessageException, "odd number of property strings"
   2.252 -        for i in xrange(0,len(proplist),2):
   2.253 -            def expand(str):
   2.254 -                if len(str)==1:
   2.255 -                    str = IncomingMessage.__expandDict.get(str,str)
   2.256 -                return str
   2.257 -            self.properties[ expand(proplist[i])] = expand(proplist[i+1])
   2.258 -        
   2.259          # Decode the body:
   2.260          if self.compressed and len(encoded)>0:
   2.261              try:
   2.262 @@ -411,7 +496,7 @@
   2.263      
   2.264      def __init__(self, connection, body=None, properties=None):
   2.265          Message.__init__(self,connection,body,properties)
   2.266 -        self.urgent = self.compressed = self.noReply = False
   2.267 +        self.urgent = self.compressed = self.noReply = self._meta = self.isError = False
   2.268          self._moreComing = True
   2.269      
   2.270      def __setitem__(self, key,val):
   2.271 @@ -435,7 +520,7 @@
   2.272          propertiesSize = out.tell()
   2.273          assert propertiesSize<65536     #FIX: Return an error instead
   2.274          
   2.275 -        body = self.body
   2.276 +        body = self.body or ""
   2.277          if self.compressed:
   2.278              z = zlib.compressobj(6,zlib.DEFLATED,31)   # window size of 31 needed for gzip format
   2.279              out.write(z.compress(body))
     3.1 --- a/Python/BLIPConnectionTest.py	Sun May 24 15:03:39 2009 -0700
     3.2 +++ b/Python/BLIPConnectionTest.py	Tue Jun 23 11:44:30 2009 -0700
     3.3 @@ -72,7 +72,8 @@
     3.4      
     3.5      def tearDown(self):
     3.6          self.connection.close()
     3.7 +        asyncore.loop() # got to give it time to negotiate close; this call should exit eventually
     3.8  
     3.9  if __name__ == '__main__':
    3.10 -    logging.basicConfig(level=logging.INFO)
    3.11 +    logging.basicConfig(level=logging.DEBUG)
    3.12      unittest.main()
     4.1 --- a/Python/BLIPListenerTest.py	Sun May 24 15:03:39 2009 -0700
     4.2 +++ b/Python/BLIPListenerTest.py	Tue Jun 23 11:44:30 2009 -0700
     4.3 @@ -42,5 +42,5 @@
     4.4              logging.info("KeyboardInterrupt")
     4.5  
     4.6  if __name__ == '__main__':
     4.7 -    logging.basicConfig(level=logging.INFO)
     4.8 +    logging.basicConfig(level=logging.DEBUG)
     4.9      unittest.main()
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/Python/CloseTestPing.py	Tue Jun 23 11:44:30 2009 -0700
     5.3 @@ -0,0 +1,36 @@
     5.4 +# CloseTestPing.py
     5.5 +# Tests the closing negotiation facilities of the BLIP 1.1 protocol
     5.6 +
     5.7 +from BLIP import Connection, OutgoingRequest
     5.8 +
     5.9 +import unittest
    5.10 +import asyncore
    5.11 +import logging
    5.12 +
    5.13 +class CloseTestPing(unittest.TestCase):
    5.14 +    
    5.15 +    def handleCloseRefusal(self, resp):
    5.16 +        logging.info("Close request was refused!")
    5.17 +    
    5.18 +    def setUp(self):
    5.19 +        self.connection = Connection( ('localhost', 1337) )
    5.20 +        self.connection.onCloseRefused = self.handleCloseRefusal
    5.21 +    
    5.22 +    def handleResponse(self, resp):
    5.23 +        logging.info("Got response...")
    5.24 +    
    5.25 +    def testClose(self):
    5.26 +        req = OutgoingRequest(self.connection, "Ping")
    5.27 +        req.response.onComplete = self.handleResponse
    5.28 +        req.send()
    5.29 +        
    5.30 +        asyncore.loop(timeout=1, count=5)
    5.31 +        
    5.32 +        self.connection.close()
    5.33 +        
    5.34 +        asyncore.loop()
    5.35 +
    5.36 +
    5.37 +if __name__ == '__main__':
    5.38 +    logging.basicConfig(level=logging.DEBUG)
    5.39 +    unittest.main()
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/Python/CloseTestPong.py	Tue Jun 23 11:44:30 2009 -0700
     6.3 @@ -0,0 +1,38 @@
     6.4 +# CloseTestPong.py
     6.5 +# Tests the closing negotiation facilities of the BLIP 1.1 protocol
     6.6 +
     6.7 +from BLIP import Listener
     6.8 +
     6.9 +import logging
    6.10 +import asyncore
    6.11 +import unittest
    6.12 +
    6.13 +class CloseTestPong(unittest.TestCase):
    6.14 +    
    6.15 +    def shouldClose(self):
    6.16 +        logging.info("Allowed to close.")
    6.17 +        return True
    6.18 +    
    6.19 +    def handleConnection(self, conn):
    6.20 +        logging.info("Accepted connection.")
    6.21 +        conn.onCloseRequest = self.shouldClose
    6.22 +    
    6.23 +    def handleRequest(self, req):
    6.24 +        resp = req.response
    6.25 +        resp.body = "Pong"
    6.26 +        resp.send()
    6.27 +    
    6.28 +    def testClose(self):
    6.29 +        listen = Listener(1337)
    6.30 +        listen.onConnected = self.handleConnection
    6.31 +        listen.onRequest = self.handleRequest
    6.32 +        
    6.33 +        try:
    6.34 +            asyncore.loop()
    6.35 +        except KeyboardInterrupt:
    6.36 +            pass
    6.37 +
    6.38 +
    6.39 +if __name__ == '__main__':
    6.40 +    logging.basicConfig(level=logging.DEBUG)
    6.41 +    unittest.main()
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/Python/asynchatPing.py	Tue Jun 23 11:44:30 2009 -0700
     7.3 @@ -0,0 +1,60 @@
     7.4 +# asynchatPing
     7.5 +# Uses asynchat
     7.6 +# Not related to BLIP - just to aid in my understanding of what's going on
     7.7 +# Sends "Ping", waits for "Pong"
     7.8 +
     7.9 +import socket
    7.10 +import asyncore
    7.11 +import asynchat
    7.12 +
    7.13 +kNumPings = 10
    7.14 +
    7.15 +class asynchatPing(asynchat.async_chat):
    7.16 +    def __init__(self, address):
    7.17 +        asynchat.async_chat.__init__(self)
    7.18 +        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    7.19 +        self.connect(address)
    7.20 +        self.set_terminator("Pong")
    7.21 +        self.pingsSent = self.pongsGot = 0
    7.22 +        self.donePing = self.donePong = False
    7.23 +    
    7.24 +    def handle_connect(self):
    7.25 +        print "Connected"
    7.26 +    
    7.27 +    def handle_close(self):
    7.28 +        print "Closed"
    7.29 +        asynchat.async_chat.handle_close(self)
    7.30 +    
    7.31 +    def collect_incoming_data(self, data):
    7.32 +        """discard data"""
    7.33 +        pass
    7.34 +    
    7.35 +    def found_terminator(self):
    7.36 +        """when we get a Pong"""
    7.37 +        print "Received 'Pong'"
    7.38 +        self.pongsGot += 1
    7.39 +        if self.pongsGot == kNumPings:
    7.40 +            print "Done ponging"
    7.41 +            self.donePong = True
    7.42 +            self.close_when_done()
    7.43 +    
    7.44 +    def ping(self):
    7.45 +        if not self.donePing:
    7.46 +            self.push("Ping")
    7.47 +            print "Sent 'Ping'"
    7.48 +            self.pingsSent += 1
    7.49 +            if self.pingsSent == kNumPings:
    7.50 +                print "Done pinging"
    7.51 +                self.donePing = True
    7.52 +    
    7.53 +    def run(self):
    7.54 +        timeout = 0
    7.55 +        while not self.donePing:
    7.56 +            self.ping()
    7.57 +            asyncore.loop(timeout=timeout, count=1)
    7.58 +        asyncore.loop()
    7.59 +        print "Done!"
    7.60 +
    7.61 +if __name__ == '__main__':
    7.62 +    ping = asynchatPing( ('localhost', 1337) )
    7.63 +    ping.run()
     8.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.2 +++ b/Python/asynchatPong.py	Tue Jun 23 11:44:30 2009 -0700
     8.3 @@ -0,0 +1,59 @@
     8.4 +# asynchatPong
     8.5 +# Listener using asynchat
     8.6 +# Not related to BLIP - just to aid in my understanding of what's going on
     8.7 +# Sends "Pong" when it gets "Ping"
     8.8 +
     8.9 +import sys
    8.10 +import traceback
    8.11 +import socket
    8.12 +import asyncore
    8.13 +import asynchat
    8.14 +
    8.15 +class asynchatPongListener(asyncore.dispatcher):
    8.16 +    def __init__(self, port):
    8.17 +        asyncore.dispatcher.__init__(self)
    8.18 +        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    8.19 +        self.bind( ('', port) )
    8.20 +        self.listen(2)
    8.21 +        self.shouldAccept = True
    8.22 +    
    8.23 +    def handle_accept(self):
    8.24 +        if self.shouldAccept:
    8.25 +            sock, addr = self.accept()
    8.26 +            self.conn = asynchatPong(sock, self)
    8.27 +            self.shouldAccept = False
    8.28 +    
    8.29 +    def handle_error(self):
    8.30 +        (typ,val,trace) = sys.exc_info()
    8.31 +        print "Listener caught: %s %s\n%s" % (typ,val,traceback.format_exc())
    8.32 +        self.close()
    8.33 +    
    8.34 +    def handle_close(self):
    8.35 +        print "Listener got close"
    8.36 +        asyncore.dispatcher.handle_close(self)
    8.37 +
    8.38 +class asynchatPong(asynchat.async_chat):
    8.39 +    def __init__(self, socket, listener):
    8.40 +        asynchat.async_chat.__init__(self, socket)
    8.41 +        self._listener = listener
    8.42 +        self.set_terminator("Ping")
    8.43 +    
    8.44 +    def collect_incoming_data(self, data):
    8.45 +        """called when arbitrary amount of data arrives. we just eat it"""
    8.46 +        pass
    8.47 +    
    8.48 +    def found_terminator(self):
    8.49 +        """called when the terminator we set is found"""
    8.50 +        print "Found 'Ping'"
    8.51 +        self.push("Pong")
    8.52 +        print "Sent 'Pong'"
    8.53 +    
    8.54 +    def handle_close(self):
    8.55 +        print "Closed; closing listener"
    8.56 +        self._listener.close()
    8.57 +        asynchat.async_chat.handle_close(self)
    8.58 +    
    8.59 +
    8.60 +if __name__ == '__main__':
    8.61 +    pong = asynchatPongListener(1337)
    8.62 +    asyncore.loop()