From 3628a5d51feb2fd2e9e673df2a856a1a5404d672 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Wed, 24 Oct 2018 10:09:07 -0400 Subject: [PATCH 01/11] #298 except for serverSentEvents - Found a case that if you slow down sse transport then both transports will be active causing a number of issues. Looks like we found that before - added completion block to see negotiate as well for #276 --- .../Transports/SRServerSentEventsTransport.m | 7 +- Tests/Mocks/SRMockSSENetworkStream.h | 5 ++ Tests/Mocks/SRMockSSENetworkStream.m | 27 ++++++- Tests/Tests/SRAutoTransportTests.m | 72 +++++++++++++++++++ .../Tests/SRServerSentEventsTransportTests.m | 11 ++- 5 files changed, 117 insertions(+), 5 deletions(-) diff --git a/SignalR.Client/Transports/SRServerSentEventsTransport.m b/SignalR.Client/Transports/SRServerSentEventsTransport.m index 281eb463..d1521388 100644 --- a/SignalR.Client/Transports/SRServerSentEventsTransport.m +++ b/SignalR.Client/Transports/SRServerSentEventsTransport.m @@ -67,7 +67,7 @@ - (BOOL)supportsKeepAlive { - (void)negotiate:(id)connection connectionData:(NSString *)connectionData completionHandler:(void (^)(SRNegotiationResponse * response, NSError *error))block { SRLogSSEDebug(@"serverSentEvents will negotiate"); - [super negotiate:connection connectionData:connectionData completionHandler:nil]; + [super negotiate:connection connectionData:connectionData completionHandler:block]; } - (void)start:(id)connection connectionData:(NSString *)connectionData completionHandler:(void (^)(id response, NSError *error))block { @@ -86,6 +86,11 @@ - (void)start:(id)connection connectionData:(NSString *)c }; NSError *timeout = [[NSError alloc]initWithDomain:[NSString stringWithFormat:NSLocalizedString(@"com.SignalR.SignalR-ObjC.%@",@""),NSStringFromClass([self class])] code:NSURLErrorTimedOut userInfo:userInfo]; SRLogSSEError(@"serverSentEvents failed to receive initialized message before timeout"); + _stop = YES; + strongSelf.eventSource.opened = nil; + strongSelf.eventSource.message = nil; + strongSelf.eventSource.closed = nil; + [strongSelf.eventSource close]; strongSelf.completionHandler(nil, timeout); strongSelf.completionHandler = nil; } diff --git a/Tests/Mocks/SRMockSSENetworkStream.h b/Tests/Mocks/SRMockSSENetworkStream.h index 0d0680f4..8cd04589 100644 --- a/Tests/Mocks/SRMockSSENetworkStream.h +++ b/Tests/Mocks/SRMockSSENetworkStream.h @@ -8,14 +8,19 @@ #import #import +#import +@class SRMockWaitBlockOperation; @interface SRMockSSENetworkStream : NSObject +@property (strong, nonatomic, readonly) NSOutputStream * stream; + - (void)prepareForOpeningResponse:(void (^)())then; - (void)prepareForOpeningResponse:(NSString *)response then:(void (^)())then; - (void)prepareForNextResponse:(NSString *)response then:(void (^)())then; - (void)prepareForClose; - (void)prepareForError:(NSError *)error; +- (void)prepareForConnectTimeout:(NSInteger)timeout beforeCaptureTimeout:(void (^)(SRMockWaitBlockOperation *transportConnectTimeout))beforeCaptureTimeout afterCaptureTimeout:(void (^)(SRMockWaitBlockOperation *transportConnectTimeout))afterCaptureTimeout; - (void)stopMocking; diff --git a/Tests/Mocks/SRMockSSENetworkStream.m b/Tests/Mocks/SRMockSSENetworkStream.m index 2bf008e8..1352e437 100644 --- a/Tests/Mocks/SRMockSSENetworkStream.m +++ b/Tests/Mocks/SRMockSSENetworkStream.m @@ -7,6 +7,7 @@ // #import "SRMockSSENetworkStream.h" +#import "SRMockWaitBlockOperation.h" #import @interface SRMockSSENetworkStream () @@ -14,6 +15,7 @@ @interface SRMockSSENetworkStream () @property (readwrite, nonatomic, strong) NSData* lastData; @property (readwrite, nonatomic, strong) id dataDelegate; @property (readwrite, nonatomic, strong) id mock; +@property (readwrite, nonatomic, strong) NSOutputStream* outputStream; //only call this directly if you don't want to trigger the stream.opened callback @property (readwrite, nonatomic, copy) void (^onSuccess)(AFHTTPRequestOperation *operation, id responseObject); @property (readwrite, nonatomic, copy) void (^onFailure)(AFHTTPRequestOperation *operation, NSError *error); @@ -47,6 +49,10 @@ - (instancetype)init { return self; } +- (NSOutputStream *)stream { + return self.outputStream; +} + - (void)prepareForOpeningResponse:(void (^)())then { return [self prepareForOpeningResponse:nil then:then]; } @@ -54,7 +60,8 @@ - (void)prepareForOpeningResponse:(void (^)())then { - (void)prepareForOpeningResponse:(NSString *)response then:(void (^)())then { NSOutputStream* dataStream = [[NSOutputStream alloc] initToMemory]; [[[self.mock stub] andReturn: dataStream] outputStream]; - + _outputStream = dataStream; + if (!response) { response = @""; } @@ -74,6 +81,24 @@ - (void)prepareForOpeningResponse:(NSString *)response then:(void (^)())then { } } + +- (void)prepareForConnectTimeout:(NSInteger)timeout beforeCaptureTimeout:(void (^)(SRMockWaitBlockOperation *))beforeCaptureTimeout afterCaptureTimeout:(void (^)(SRMockWaitBlockOperation *))afterCaptureTimeout{ + //note: even though it's a connect timeout, we want an outputstream + //so that we can verify it closes + _outputStream = [[NSOutputStream alloc] initToMemory]; + [_outputStream open]; + [[[self.mock stub] andReturn: _outputStream] outputStream]; + + SRMockWaitBlockOperation* transportConnectTimeout = [[SRMockWaitBlockOperation alloc] initWithWaitTime:10]; + if (beforeCaptureTimeout) { + beforeCaptureTimeout(transportConnectTimeout); + } + [transportConnectTimeout stopMocking]; + if (afterCaptureTimeout) { + afterCaptureTimeout(transportConnectTimeout); + } +} + - (void)prepareForNextResponse:(NSString *)response then:(void (^)())then { NSMutableData* prior = [[NSMutableData alloc] initWithData: _lastData]; NSData* data = [response dataUsingEncoding:NSUTF8StringEncoding]; diff --git a/Tests/Tests/SRAutoTransportTests.m b/Tests/Tests/SRAutoTransportTests.m index 9688777f..425230c4 100644 --- a/Tests/Tests/SRAutoTransportTests.m +++ b/Tests/Tests/SRAutoTransportTests.m @@ -14,10 +14,16 @@ #import "SRAutoTransport.h" #import "SRWebSocketTransport.h" #import "SRServerSentEventsTransport.h" +#import "SRLongPollingTransport.h" #import "SRMockClientTransport.h" #import "SRMockWaitBlockOperation.h" #import "SRMockWSNetworkStream.h" #import "SRMockSSENetworkStream.h" +#import "SRMockNetwork.h" + +@interface SRLongPollingTransport () +@property (strong, nonatomic, readwrite) NSOperationQueue *pollingOperationQueue; +@end @interface SRAutoTransport (UnitTest) @@ -109,5 +115,71 @@ - (void)testSlowToInitializeWebsocketCleansUpAndTriesNextTransport { XCTAssert([[autoTransport name] isEqualToString:[sse name]]); } +- (void)testSlowToInitializeServerSentEventsCleansUpAndTriesNextTransport { + XCTestExpectation *initialized = [self expectationWithDescription:@"Handler called"]; + + SRConnection* connection = [[SRConnection alloc] initWithURLString:@"http://localhost:0000"]; + + SRServerSentEventsTransport *sse = [[SRServerSentEventsTransport alloc] init]; + [sse setServerSentEventsOperationQueue:nil]; + + id mockSSETransport = [OCMockObject partialMockForObject:sse]; + [[[mockSSETransport expect] andForwardToRealObject] start:[OCMArg any] connectionData:[OCMArg any] completionHandler:[OCMArg any]]; + + SRLongPollingTransport *lp = [[SRLongPollingTransport alloc] init]; + lp.pollingOperationQueue = nil;////set to nil to get around weird ARC OCMock bugs http://stackoverflow.com/questions/18121902/using-ocmock-on-nsoperation-gives-bad-access + + id mockLPTransport = [OCMockObject partialMockForObject:lp]; + [[[mockLPTransport expect] andForwardToRealObject] start:[OCMArg any] connectionData:[OCMArg any] completionHandler:[OCMArg any]]; + + SRAutoTransport* autoTransport = [[SRAutoTransport alloc] initWithTransports:@[sse, lp]]; + + id json = @{ + @"ConnectionId": @"10101", + @"ConnectionToken": @"10101010101", + @"DisconnectTimeout": @30, + @"ProtocolVersion": @"1.3.0.0", + @"TransportConnectTimeout": @10, + @"TryWebSockets": @NO + }; + [SRMockClientTransport negotiateForTransport:autoTransport statusCode:@200 json:json]; + __block id connect = nil; + + connection.started = ^{ + [initialized fulfill];//note, we wont fulfill this till we've fallen back to longPolling + [connect stopMocking];//if we dont do this we create a tight loop as longpolling will + //instantly return more data + }; + + SRMockSSENetworkStream* sseNetworkStream = [[SRMockSSENetworkStream alloc] init]; + //cannot OCMock bridge free classes + // [[[sseNetworkStream stream] expect] close]; + + + //Setup SSE to get timed out so we fallback + [sseNetworkStream prepareForConnectTimeout:10 beforeCaptureTimeout:^(SRMockWaitBlockOperation *transportConnectTimeout){ + //Start the connection and capture the performSelector blocks + //for use later + [connection start:autoTransport]; + } afterCaptureTimeout:^(SRMockWaitBlockOperation *transportConnectTimeout){ + //this is before the timeout has occurred, so last minute mock + //the request + connect = [SRMockNetwork mockHttpRequestOperationForClass:[AFHTTPRequestOperation class] + statusCode:@200 + responseString:@"abcdefg"]; + transportConnectTimeout.afterWait();//calls the timeout method, afterWait is the block passed + //at this point we should have fallen back to longPolling + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { + if (error) { + NSLog(@"Timeout Error: %@", error); + } + }]; + [mockSSETransport verify]; + XCTAssertEqual([[sseNetworkStream stream] streamStatus], NSStreamStatusClosed); + + XCTAssert([[autoTransport name] isEqualToString:[lp name]]); +} @end diff --git a/Tests/Tests/SRServerSentEventsTransportTests.m b/Tests/Tests/SRServerSentEventsTransportTests.m index 120a6c21..c77c204b 100644 --- a/Tests/Tests/SRServerSentEventsTransportTests.m +++ b/Tests/Tests/SRServerSentEventsTransportTests.m @@ -52,17 +52,17 @@ - (void)testStartCallsTheCompletionHandlerAfterSuccess { SRConnection* connection = [[SRConnection alloc] initWithURLString:@"http://localhost:0000"]; connection.connectionToken = @"10101010101"; connection.connectionId = @"10101"; + connection.transportConnectTimeout = @10; [connection changeState:disconnected toState:connected]; SRServerSentEventsTransport* sse = [[SRServerSentEventsTransport alloc] init]; sse.serverSentEventsOperationQueue = nil;//set to nil to get around weird ARC OCMock bugs http://stackoverflow.com/questions/18121902/using-ocmock-on-nsoperation-gives-bad-access - - [NetworkMock prepareForOpeningResponse:^{ + //note: SSE sends down initialized but it gets ignored in all clients + [NetworkMock prepareForOpeningResponse:@"data: initialized\n\ndata: {}\n" then:^{ [sse start: connection connectionData:@"12345" completionHandler:^(id response, NSError *error){ [expectation fulfill]; }]; }]; - [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { if (error) { NSLog(@"Timeout Error: %@", error); @@ -78,6 +78,7 @@ - (void)testParsesInitialBuffer { SRConnection* connection = [[SRConnection alloc] initWithURLString:@"http://localhost:0000"]; connection.connectionToken = @"10101010101"; connection.connectionId = @"10101"; + connection.transportConnectTimeout = @10; [connection changeState:disconnected toState:connected]; SRServerSentEventsTransport* sse = [[SRServerSentEventsTransport alloc] init]; @@ -104,6 +105,7 @@ - (void)testIgnoresInitializedAndEmptyLinesWhenParsingMessages { SRConnection* connection = [[SRConnection alloc] initWithURLString:@"http://localhost:0000"]; connection.connectionToken = @"10101010101"; connection.connectionId = @"10101"; + connection.transportConnectTimeout = @10; [connection changeState:disconnected toState:connected]; connection.received = ^(NSDictionary * data){ @@ -134,6 +136,7 @@ - (void)testConnectionInitialFailureUsesCallback { SRConnection* connection = [[SRConnection alloc] initWithURLString:@"http://localhost:0000"]; connection.connectionToken = @"10101010101"; connection.connectionId = @"10101"; + connection.transportConnectTimeout = @10; [connection changeState:disconnected toState:connected]; SRServerSentEventsTransport* sse = [[SRServerSentEventsTransport alloc] init]; @@ -354,6 +357,7 @@ - (void)testHandlesExtraEmptyLinesWhenParsingMessages { SRConnection* connection = [[SRConnection alloc] initWithURLString:@"http://localhost:0000"]; connection.connectionToken = @"10101010101"; connection.connectionId = @"10101"; + connection.transportConnectTimeout = @10; [connection changeState:disconnected toState:connected]; connection.received = ^(NSString * data){ @@ -383,6 +387,7 @@ - (void)testHandlesNewLinesSpreadOutOverReads { SRConnection* connection = [[SRConnection alloc]initWithURLString:@"http://localhost:0000"]; connection.connectionToken = @"10101010101"; connection.connectionId = @"10101"; + connection.transportConnectTimeout = @10; [connection changeState:disconnected toState:connected]; connection.received = ^(NSDictionary * data){ From 041ce77ecc9600424507dd50401c645ccf71c0af Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Thu, 25 Oct 2018 08:29:18 -0400 Subject: [PATCH 02/11] #298 updating pod file, etc --- Podfile | 4 +- Podfile.lock | 10 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ .../xcshareddata/WorkspaceSettings.xcsettings | 5 + .../project.pbxproj | 122 +++++++++++++++--- 5 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 SignalR.Client.ObjC.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 SignalR.Client.ObjC.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/Podfile b/Podfile index 7ed01693..30db80c0 100644 --- a/Podfile +++ b/Podfile @@ -1,4 +1,4 @@ -xcodeproj 'SignalR.Client.ObjC/SignalR.Client.ObjC' +project 'SignalR.Client.ObjC/SignalR.Client.ObjC.xcodeproj' workspace 'SignalR.Client.ObjC' target "SignalR.Client.iOS" do @@ -23,4 +23,4 @@ target :"SignalR.Client.OSX" do target :"SignalR.Client.OSXTests" do pod 'OCMock' end -end \ No newline at end of file +end diff --git a/Podfile.lock b/Podfile.lock index 3c53754a..2af314c5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -28,9 +28,17 @@ DEPENDENCIES: - OCMock - SocketRocket (= 0.4.2) +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - AFNetworking + - OCMock + - SocketRocket + SPEC CHECKSUMS: AFNetworking: cb8d14a848e831097108418f5d49217339d4eb60 OCMock: 18c9b7e67d4c2770e95bb77a9cc1ae0c91fe3835 SocketRocket: ffe08119b00ef982f6c37052a4705a057c8494ad -COCOAPODS: 0.39.0 +PODFILE CHECKSUM: b3a3a58c75f13f9a520b6c950906fdd364953a26 + +COCOAPODS: 1.5.3 diff --git a/SignalR.Client.ObjC.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SignalR.Client.ObjC.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/SignalR.Client.ObjC.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SignalR.Client.ObjC.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/SignalR.Client.ObjC.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/SignalR.Client.ObjC.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/SignalR.Client.ObjC/SignalR.Client.ObjC.xcodeproj/project.pbxproj b/SignalR.Client.ObjC/SignalR.Client.ObjC.xcodeproj/project.pbxproj index a8f1b0cc..6246c7d6 100644 --- a/SignalR.Client.ObjC/SignalR.Client.ObjC.xcodeproj/project.pbxproj +++ b/SignalR.Client.ObjC/SignalR.Client.ObjC.xcodeproj/project.pbxproj @@ -53,7 +53,6 @@ 39302A211C69768D0061C6B5 /* SRServerSentEventsTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 3920786115AF13F6009B959E /* SRServerSentEventsTransport.m */; }; 39302A221C69768D0061C6B5 /* SRWebSocketTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 39AF0D7D17138E3800E13E6E /* SRWebSocketTransport.m */; }; 39302A231C6976A30061C6B5 /* SRSubscription.m in Sources */ = {isa = PBXBuildFile; fileRef = 3920783615AF13F6009B959E /* SRSubscription.m */; }; - 39302A271C6977800061C6B5 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 39302A251C6977800061C6B5 /* Info.plist */; }; 39302A281C6977800061C6B5 /* SignalR.h in Headers */ = {isa = PBXBuildFile; fileRef = 39302A261C6977800061C6B5 /* SignalR.h */; }; 39302A381C6978AC0061C6B5 /* SignalR.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39302A2E1C6978AB0061C6B5 /* SignalR.framework */; }; 39302A451C6979E10061C6B5 /* SRHubConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 3920782715AF13F6009B959E /* SRHubConnection.m */; }; @@ -522,12 +521,12 @@ isa = PBXNativeTarget; buildConfigurationList = 39302A051C6974C20061C6B5 /* Build configuration list for PBXNativeTarget "SignalR.Client.OSX" */; buildPhases = ( + 759DD48E5A906D4D2714F305 /* [CP] Check Pods Manifest.lock */, BE773CA6D603623DBCDFD6AF /* Check Pods Manifest.lock */, 393029EF1C6974C10061C6B5 /* Sources */, 393029F01C6974C10061C6B5 /* Frameworks */, 393029F11C6974C10061C6B5 /* Headers */, 393029F21C6974C10061C6B5 /* Resources */, - 7C5BAF419D1DB75581A41B87 /* Copy Pods Resources */, ); buildRules = ( ); @@ -542,12 +541,13 @@ isa = PBXNativeTarget; buildConfigurationList = 39302A081C6974C20061C6B5 /* Build configuration list for PBXNativeTarget "SignalR.Client.OSXTests" */; buildPhases = ( + 29F8A99392528EEAD6A859C3 /* [CP] Check Pods Manifest.lock */, 537C4819A81E84FE2982C1CF /* Check Pods Manifest.lock */, 393029F91C6974C20061C6B5 /* Sources */, 393029FA1C6974C20061C6B5 /* Frameworks */, 393029FB1C6974C20061C6B5 /* Resources */, 67E90B84652BAD667E4611D1 /* Embed Pods Frameworks */, - 0B029DDB5BC07F36BE28C47C /* Copy Pods Resources */, + ED3F4313A751EA87E66F745B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -563,12 +563,12 @@ isa = PBXNativeTarget; buildConfigurationList = 39302A3F1C6978AC0061C6B5 /* Build configuration list for PBXNativeTarget "SignalR.Client.iOS" */; buildPhases = ( + A48F4A4D58939ECEEB8E687E /* [CP] Check Pods Manifest.lock */, 42E081C85DE4F4916D0F3D75 /* Check Pods Manifest.lock */, 39302A291C6978AB0061C6B5 /* Sources */, 39302A2A1C6978AB0061C6B5 /* Frameworks */, 39302A2B1C6978AB0061C6B5 /* Headers */, 39302A2C1C6978AB0061C6B5 /* Resources */, - 796268F885EFEC8F1356854A /* Copy Pods Resources */, ); buildRules = ( ); @@ -583,12 +583,13 @@ isa = PBXNativeTarget; buildConfigurationList = 39302A421C6978AC0061C6B5 /* Build configuration list for PBXNativeTarget "SignalR.Client.iOSTests" */; buildPhases = ( + EF8CD3D9CD825628C4319223 /* [CP] Check Pods Manifest.lock */, 21C56E6B6FDFA0220435E7AD /* Check Pods Manifest.lock */, 39302A331C6978AC0061C6B5 /* Sources */, 39302A341C6978AC0061C6B5 /* Frameworks */, 39302A351C6978AC0061C6B5 /* Resources */, 2AC719B5534D658A6E1038BA /* Embed Pods Frameworks */, - 1A11AFB25C13B6568AF7E084 /* Copy Pods Resources */, + 239C11727851DAB652DA48A3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -648,7 +649,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 39302A271C6977800061C6B5 /* Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -676,49 +676,67 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0B029DDB5BC07F36BE28C47C /* Copy Pods Resources */ = { + 21C56E6B6FDFA0220435E7AD /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Copy Pods Resources"; + name = "Check Pods Manifest.lock"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests-resources.sh\"\n"; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; - 1A11AFB25C13B6568AF7E084 /* Copy Pods Resources */ = { + 239C11727851DAB652DA48A3 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.iOS-SignalR.Client.iOSTests/Pods-SignalR.Client.iOS-SignalR.Client.iOSTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework", + "${BUILT_PRODUCTS_DIR}/SocketRocket-iOS/SocketRocket.framework", + "${BUILT_PRODUCTS_DIR}/OCMock-iOS/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( ); - name = "Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AFNetworking.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.iOS-SignalR.Client.iOSTests/Pods-SignalR.Client.iOS-SignalR.Client.iOSTests-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.iOS-SignalR.Client.iOSTests/Pods-SignalR.Client.iOS-SignalR.Client.iOSTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 21C56E6B6FDFA0220435E7AD /* Check Pods Manifest.lock */ = { + 29F8A99392528EEAD6A859C3 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 2AC719B5534D658A6E1038BA /* Embed Pods Frameworks */ = { @@ -781,34 +799,48 @@ shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 796268F885EFEC8F1356854A /* Copy Pods Resources */ = { + 759DD48E5A906D4D2714F305 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Copy Pods Resources"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SignalR.Client.OSX-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.iOS/Pods-SignalR.Client.iOS-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 7C5BAF419D1DB75581A41B87 /* Copy Pods Resources */ = { + A48F4A4D58939ECEEB8E687E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Copy Pods Resources"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SignalR.Client.iOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.OSX/Pods-SignalR.Client.OSX-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; BE773CA6D603623DBCDFD6AF /* Check Pods Manifest.lock */ = { @@ -826,6 +858,54 @@ shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; + ED3F4313A751EA87E66F745B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/AFNetworking-93a4f461/AFNetworking.framework", + "${BUILT_PRODUCTS_DIR}/SocketRocket-macOS/SocketRocket.framework", + "${BUILT_PRODUCTS_DIR}/OCMock-macOS/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + ); + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AFNetworking.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests/Pods-SignalR.Client.OSX-SignalR.Client.OSXTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EF8CD3D9CD825628C4319223 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SignalR.Client.iOS-SignalR.Client.iOSTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ From 81d86f64ed26ce3162cd8e55ee593d90e99f49d0 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Thu, 1 Nov 2018 15:33:12 -0400 Subject: [PATCH 03/11] #298 do not implicitly retain --- SignalR.Client/Transports/SRServerSentEventsTransport.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SignalR.Client/Transports/SRServerSentEventsTransport.m b/SignalR.Client/Transports/SRServerSentEventsTransport.m index d1521388..efbc6a45 100644 --- a/SignalR.Client/Transports/SRServerSentEventsTransport.m +++ b/SignalR.Client/Transports/SRServerSentEventsTransport.m @@ -86,7 +86,7 @@ - (void)start:(id)connection connectionData:(NSString *)c }; NSError *timeout = [[NSError alloc]initWithDomain:[NSString stringWithFormat:NSLocalizedString(@"com.SignalR.SignalR-ObjC.%@",@""),NSStringFromClass([self class])] code:NSURLErrorTimedOut userInfo:userInfo]; SRLogSSEError(@"serverSentEvents failed to receive initialized message before timeout"); - _stop = YES; + strongSelf._stop = YES; strongSelf.eventSource.opened = nil; strongSelf.eventSource.message = nil; strongSelf.eventSource.closed = nil; From a2415dc906f4da5fe29b3b15e073e7b79073e766 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Thu, 1 Nov 2018 16:04:50 -0400 Subject: [PATCH 04/11] #298 typo - do not implicitly retain --- SignalR.Client/Transports/SRServerSentEventsTransport.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SignalR.Client/Transports/SRServerSentEventsTransport.m b/SignalR.Client/Transports/SRServerSentEventsTransport.m index efbc6a45..b02ff6df 100644 --- a/SignalR.Client/Transports/SRServerSentEventsTransport.m +++ b/SignalR.Client/Transports/SRServerSentEventsTransport.m @@ -86,7 +86,7 @@ - (void)start:(id)connection connectionData:(NSString *)c }; NSError *timeout = [[NSError alloc]initWithDomain:[NSString stringWithFormat:NSLocalizedString(@"com.SignalR.SignalR-ObjC.%@",@""),NSStringFromClass([self class])] code:NSURLErrorTimedOut userInfo:userInfo]; SRLogSSEError(@"serverSentEvents failed to receive initialized message before timeout"); - strongSelf._stop = YES; + strongSelf.stop = YES; strongSelf.eventSource.opened = nil; strongSelf.eventSource.message = nil; strongSelf.eventSource.closed = nil; From c5b35577ede32fde63e5000a707e8324e5157c48 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Mon, 26 Nov 2018 09:32:08 -0500 Subject: [PATCH 05/11] expose transport for use in tests, etc. - by default you should not be messing with transport on the connection. we needed to for our use case and I could not figure out another way to get access to this other than exposing it. --- SignalR.Client/SRConnection.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SignalR.Client/SRConnection.m b/SignalR.Client/SRConnection.m index fcd23734..96e42f7f 100644 --- a/SignalR.Client/SRConnection.m +++ b/SignalR.Client/SRConnection.m @@ -45,6 +45,8 @@ @interface SRConnection () @property (strong, nonatomic, readwrite) NSString *connectionData; @property (strong, nonatomic, readwrite) SRHeartbeatMonitor *monitor; +@property (strong, nonatomic, readwrite) id transport; + - (void)negotiate:(id )transport; - (void)verifyProtocolVersion:(NSString *)versionString; - (NSString *)createUserAgentString:(NSString *)client; From 5d92e47903af952d004bf7f52c0e7fd6bd38705d Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Mon, 26 Nov 2018 17:10:32 -0500 Subject: [PATCH 06/11] #298 : clear out handlers when there is a completionHandler - sse will continue reconnecting even if the connection is no longer referencing the transport if you use it directly. dereference everything so that it will not attempt to reconnect after being stopped --- SignalR.Client/Transports/SRServerSentEventsTransport.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SignalR.Client/Transports/SRServerSentEventsTransport.m b/SignalR.Client/Transports/SRServerSentEventsTransport.m index b02ff6df..683675d2 100644 --- a/SignalR.Client/Transports/SRServerSentEventsTransport.m +++ b/SignalR.Client/Transports/SRServerSentEventsTransport.m @@ -250,9 +250,14 @@ - (void)open:(id )connection connectionData:(NSString *)c selector:@selector(start) object:nil]; self.connectTimeoutOperation = nil; - + strongSelf.stop = YES; + strongSelf.eventSource.opened = nil; + strongSelf.eventSource.message = nil; + strongSelf.eventSource.closed = nil; + [strongSelf.eventSource close]; strongSelf.completionHandler(nil, error); strongSelf.completionHandler = nil; + strongSelf.eventSource = nil; } else if (!isReconnecting){//failure should first attempt to reconect SRLogSSEWarn(@"will reconnect from errors: %@", error); } else {//failure while reconnecting should error From 5227bd1dc1303dcb2dcf6c860535e97f061c9200 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Mon, 26 Nov 2018 17:14:58 -0500 Subject: [PATCH 07/11] prevent longPolling from running forever - when testing if you either timeout or abort connections (using fiddler or some other mitm tool) on most connections, they fallback regularly, but if you ever get to longPolling, it would continue retrying forever - even if there no connection ever comes up. This change checks if we are active when it gets an error in longPolling. If outside the reconnectWindow, it will stop the connection. --- SignalR.Client/SRConnection.m | 32 +++++++++++++- SignalR.Client/SRConnectionInterface.h | 2 + SignalR.Client/SRHeartbeatMonitor.h | 1 + SignalR.Client/SRHeartbeatMonitor.m | 42 +++++++++++-------- .../Transports/SRLongPollingTransport.m | 4 ++ 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/SignalR.Client/SRConnection.m b/SignalR.Client/SRConnection.m index 96e42f7f..f9cd5b94 100644 --- a/SignalR.Client/SRConnection.m +++ b/SignalR.Client/SRConnection.m @@ -46,6 +46,8 @@ @interface SRConnection () @property (strong, nonatomic, readwrite) SRHeartbeatMonitor *monitor; @property (strong, nonatomic, readwrite) id transport; +@property (strong, nonatomic, readwrite) NSDate* lastActive; +@property (strong, nonatomic, readwrite) NSNumber* reconnectWindow; - (void)negotiate:(id )transport; - (void)verifyProtocolVersion:(NSString *)versionString; @@ -69,6 +71,9 @@ @implementation SRConnection @synthesize transport = _transport; @synthesize credentials = _credentials; @synthesize headers = _headers; +@synthesize lastActive = _lastActive; +@synthesize reconnectWindow = _reconnectWindow; + #pragma mark - #pragma mark Initialization @@ -148,7 +153,10 @@ - (void)negotiate:(id)transport { // If we have a keep alive if (negotiationResponse.keepAliveTimeout != nil) { - _keepAliveData = [[SRKeepAliveData alloc] initWithTimeout:negotiationResponse.keepAliveTimeout]; + strongSelf.keepAliveData = [[SRKeepAliveData alloc] initWithTimeout:negotiationResponse.keepAliveTimeout]; + strongSelf.reconnectWindow = @([strongSelf.disconnectTimeout floatValue] + [strongSelf.keepAliveData.timeout floatValue]); + } else { + strongSelf.reconnectWindow = strongSelf.disconnectTimeout; } [strongSelf startTransport]; @@ -418,6 +426,28 @@ - (void)updateLastKeepAlive { } } +-(void)markActive{ + if([self verifyLastActive]){ + _lastActive = [NSDate date]; + } +} + +- (BOOL) verifyLastActive { + if (_lastActive == nil || _reconnectWindow == nil){ + //todo figure out what really should go here + _lastActive = [NSDate date]; + return YES; + } + NSTimeInterval timeElapsed = [[NSDate date] timeIntervalSinceDate: _lastActive];//number of seconds + if (timeElapsed > [_reconnectWindow floatValue]){ + SRLogConnectionDebug(@"There has not been an active server connection for an extended period of time. Stopping connection."); + [self stop:_defaultAbortTimeout]; + return NO; + } + return YES; +} + + - (void)prepareRequest:(NSMutableURLRequest *)request { #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR [request addValue:[self createUserAgentString:NSLocalizedString(@"SignalR.Client.iOS",@"")] forHTTPHeaderField:@"User-Agent"]; diff --git a/SignalR.Client/SRConnectionInterface.h b/SignalR.Client/SRConnectionInterface.h index 92d72ee1..2785fa41 100644 --- a/SignalR.Client/SRConnectionInterface.h +++ b/SignalR.Client/SRConnectionInterface.h @@ -53,6 +53,8 @@ ///------------------------------- - (NSString *)onSending;//TODO: this just encapsulates connectionData. can we pull this into a getUrl like js client does? +- (BOOL) verifyLastActive; +- (void) markActive; ///------------------------------- /// @name Connection Management diff --git a/SignalR.Client/SRHeartbeatMonitor.h b/SignalR.Client/SRHeartbeatMonitor.h index 8a0a4d4b..b999ea0a 100644 --- a/SignalR.Client/SRHeartbeatMonitor.h +++ b/SignalR.Client/SRHeartbeatMonitor.h @@ -28,6 +28,7 @@ @property (assign, nonatomic, readonly, getter = hasBeenWarned) BOOL beenWarned; @property (assign, nonatomic, readonly) BOOL timedOut; +@property (assign, nonatomic, readonly) BOOL monitorKeepAlive; - (instancetype)initWithConnection:(id )connection; - (void)start; diff --git a/SignalR.Client/SRHeartbeatMonitor.m b/SignalR.Client/SRHeartbeatMonitor.m index 3417c29f..5388969f 100644 --- a/SignalR.Client/SRHeartbeatMonitor.m +++ b/SignalR.Client/SRHeartbeatMonitor.m @@ -44,7 +44,10 @@ - (instancetype)initWithConnection:(id )connection { } - (void)start { - [_connection updateLastKeepAlive]; + _monitorKeepAlive = [_connection keepAliveData] && [_connection.transport supportsKeepAlive]; + if (_monitorKeepAlive){ + [_connection updateLastKeepAlive]; + } _beenWarned = NO; _timedOut = NO; _timer = [NSTimer scheduledTimerWithTimeInterval:[[[_connection keepAliveData] checkInterval] integerValue] @@ -60,26 +63,29 @@ - (void)heartbeat:(NSTimer *)timer { } - (void)beat:(NSInteger)timeElapsed { - if (_connection.state == connected) { - if (timeElapsed >= [[[_connection keepAliveData] timeout] integerValue]) { - if (!self.timedOut) { - // Connection has been lost - SRLogConnectionWarn(@"Connection Timed-out : Transport Lost Connection"); - _timedOut = true; - [[_connection transport] lostConnection:_connection]; - } - } else if (timeElapsed >= [[[_connection keepAliveData] timeoutWarning] integerValue]) { - if (!self.hasBeenWarned) { - // Inform user and set HasBeenWarned to true - SRLogConnectionWarn(@"Connection Timeout Warning : Notifying user"); - _beenWarned = true; - [_connection connectionDidSlow]; + if (_monitorKeepAlive){ + if (_connection.state == connected) { + if (timeElapsed >= [[[_connection keepAliveData] timeout] integerValue]) { + if (!self.timedOut) { + // Connection has been lost + SRLogConnectionWarn(@"Connection Timed-out : Transport Lost Connection"); + _timedOut = true; + [[_connection transport] lostConnection:_connection]; + } + } else if (timeElapsed >= [[[_connection keepAliveData] timeoutWarning] integerValue]) { + if (!self.hasBeenWarned) { + // Inform user and set HasBeenWarned to true + SRLogConnectionWarn(@"Connection Timeout Warning : Notifying user"); + _beenWarned = true; + [_connection connectionDidSlow]; + } + } else { + _beenWarned = false; + _timedOut = false; } - } else { - _beenWarned = false; - _timedOut = false; } } + [_connection markActive]; } - (void)stop { diff --git a/SignalR.Client/Transports/SRLongPollingTransport.m b/SignalR.Client/Transports/SRLongPollingTransport.m index 11c7b48c..d2352384 100644 --- a/SignalR.Client/Transports/SRLongPollingTransport.m +++ b/SignalR.Client/Transports/SRLongPollingTransport.m @@ -172,6 +172,10 @@ - (void)poll:(id)connection connectionData:(NSString *)co canReconnect = @(NO); + if (![strongConnection verifyLastActive]){ + return;//connection will have aborted above + } + // Transition into reconnecting state [SRConnection ensureReconnecting:strongConnection]; From 157a780adec1683b234c05291b570bb077ce78a8 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Wed, 28 Nov 2018 17:08:16 -0500 Subject: [PATCH 08/11] #300 : trigger didReconnect on initialized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 503 errors or firewall software, etc will hit the NSStreamEventOpenCompleted and thus the onOpened callback. But we do not want to trigger didReconnect here or else we’ll never timeout our reconnect. Instead, process the initialized message that is sent down. --- .../Transports/SRServerSentEventsTransport.m | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/SignalR.Client/Transports/SRServerSentEventsTransport.m b/SignalR.Client/Transports/SRServerSentEventsTransport.m index 683675d2..bae78b98 100644 --- a/SignalR.Client/Transports/SRServerSentEventsTransport.m +++ b/SignalR.Client/Transports/SRServerSentEventsTransport.m @@ -172,6 +172,15 @@ - (void)open:(id )connection connectionData:(NSString *)c NSString *data = [[NSString alloc] initWithData:sseEvent.data encoding:NSUTF8StringEncoding]; SRLogSSEInfo(@"serverSentEvents did receive: %@", data); if([data caseInsensitiveCompare:@"initialized"] == NSOrderedSame) { + // SSE can get a 503 error, terminated connection, etc + // which should not trigger reconnect, so instead the server sends + // down initialized. This is sufficient for knowing we have + // reconnected. + // This will noop if we're not in the reconnecting state + if([strongConnection changeState:reconnecting toState:connected]) { + // Raise the reconnect event if the connection comes back up + [strongConnection didReconnect]; + } return; } @@ -220,8 +229,7 @@ - (void)open:(id )connection connectionData:(NSString *)c [strongSelf completeAbort]; } else if ([strongSelf tryCompleteAbort]) { - } - else { + } else if ([strongConnection verifyLastActive]){//check in if we should abandon [strongSelf reconnect:strongConnection data:connectionData]; } }; @@ -233,7 +241,7 @@ - (void)open:(id )connection connectionData:(NSString *)c if (strongSelf.stop) { [strongSelf completeAbort]; } else if ([strongSelf tryCompleteAbort]) { - } else { + } else if ([strongConnection verifyLastActive]){//check in if we should abandon [strongSelf reconnect:strongConnection data:connectionData]; } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { From 09460fdfebf57e41ea3483e908777cac4f3daa77 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Thu, 29 Nov 2018 10:32:13 -0500 Subject: [PATCH 09/11] #301 : move from bool to block - the bool was not always catching our reconnect and was a little more difficult to track what was going on. Instead store the blockoperation and cancel in the right places --- .../Transports/SRLongPollingTransport.m | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/SignalR.Client/Transports/SRLongPollingTransport.m b/SignalR.Client/Transports/SRLongPollingTransport.m index d2352384..06a6590a 100644 --- a/SignalR.Client/Transports/SRLongPollingTransport.m +++ b/SignalR.Client/Transports/SRLongPollingTransport.m @@ -27,11 +27,13 @@ #import "SRLog.h" #import "SRLongPollingTransport.h" - @interface SRLongPollingTransport() - - @property (strong, nonatomic, readwrite) NSOperationQueue *pollingOperationQueue; - - @end +@interface SRLongPollingTransport() + +@property (strong, nonatomic, readwrite) NSOperationQueue *pollingOperationQueue; +@property (nonatomic, readwrite) int reconnectErrors; +@property (strong, nonatomic, readwrite) NSBlockOperation *reconnectTimeout; + +@end @implementation SRLongPollingTransport @@ -63,6 +65,7 @@ - (void)negotiate:(id)connection connectionData:(NSString - (void)start:(id)connection connectionData:(NSString *)connectionData completionHandler:(void (^)(id response, NSError *error))block { SRLogLPDebug(@"longPolling will connect with connectionData %@", connectionData); + self.reconnectErrors = 0; [self poll:connection connectionData:connectionData completionHandler:block]; } @@ -73,6 +76,9 @@ - (void)send:(id)connection data:(NSString *)data connect - (void)abort:(id)connection timeout:(NSNumber *)timeout connectionData:(NSString *)connectionData { SRLogLPDebug(@"longPolling will abort"); + [[self reconnectTimeout] cancel]; + self.reconnectTimeout = nil; + [super abort:connection timeout:timeout connectionData:connectionData]; } @@ -85,8 +91,6 @@ - (void)lostConnection:(id)connection { - (void)poll:(id)connection connectionData:(NSString *)connectionData completionHandler:(void (^)(id response, NSError *error))block { - __block NSNumber *canReconnect = @(YES); - NSString *url = connection.url; if(connection.messageId == nil) { url = [url stringByAppendingString:@"connect"]; @@ -96,7 +100,7 @@ - (void)poll:(id)connection connectionData:(NSString *)co url = [url stringByAppendingString:@"poll"]; } - [self delayConnectionReconnect:connection canReconnect:canReconnect]; + [self delayConnectionReconnect:connection]; __weak __typeof(&*self)weakSelf = self; __weak __typeof(&*connection)weakConnection = connection; @@ -128,9 +132,12 @@ - (void)poll:(id)connection connectionData:(NSString *)co [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { __strong __typeof(&*weakSelf)strongSelf = weakSelf; __strong __typeof(&*weakConnection)strongConnection = weakConnection; - + BOOL shouldReconnect = NO; BOOL disconnectedReceived = NO; + strongSelf.reconnectErrors = 0; + [[strongSelf reconnectTimeout] cancel]; + strongSelf.reconnectTimeout = nil; SRLogLPInfo(@"longPolling did receive: %@", operation.responseString); @@ -143,7 +150,7 @@ - (void)poll:(id)connection connectionData:(NSString *)co // If the timeout for the reconnect hasn't fired as yet just fire the // event here before any incoming messages are processed SRLogLPWarn(@"reconnecting"); - [strongSelf connectionReconnect:strongConnection canReconnect:canReconnect]; + [strongSelf connectionReconnect:strongConnection]; } if (shouldReconnect) { @@ -159,7 +166,6 @@ - (void)poll:(id)connection connectionData:(NSString *)co if (![strongSelf tryCompleteAbort]) { //Abort has not been called so continue polling... - canReconnect = @(YES); [strongSelf poll:strongConnection connectionData:connectionData completionHandler:nil]; } else { SRLogLPWarn(@"longPolling has shutdown due to abort"); @@ -167,10 +173,13 @@ - (void)poll:(id)connection connectionData:(NSString *)co } failure:^(AFHTTPRequestOperation *operation, NSError *error) { __strong __typeof(&*weakSelf)strongSelf = weakSelf; __strong __typeof(&*weakConnection)strongConnection = weakConnection; - + SRLogLPError(@"longPolling did fail with error %@", error); - canReconnect = @(NO); + strongSelf.reconnectErrors++; + [[strongSelf reconnectTimeout] cancel];//cancel the isalive reconnect so we dont falsely assume we succeeded + strongSelf.reconnectTimeout = nil; + if (![strongConnection verifyLastActive]){ return;//connection will have aborted above @@ -185,8 +194,6 @@ - (void)poll:(id)connection connectionData:(NSString *)co SRLogLPDebug(@"will poll again in %ld seconds",(long)[_errorDelay integerValue]); - canReconnect = @(YES); - [[NSBlockOperation blockOperationWithBlock:^{ [strongSelf poll:strongConnection connectionData:connectionData completionHandler:nil]; }] performSelector:@selector(start) withObject:nil afterDelay:[strongSelf.errorDelay integerValue]]; @@ -201,30 +208,29 @@ - (void)poll:(id)connection connectionData:(NSString *)co [self.pollingOperationQueue addOperation:operation]; } -- (void)delayConnectionReconnect:(id)connection canReconnect:(NSNumber *)canReconnect { +- (void)delayConnectionReconnect:(id)connection { if ([self isConnectionReconnecting:connection]) { __weak __typeof(&*self)weakSelf = self; __weak __typeof(&*connection)weakConnection = connection; - __weak __typeof(&*canReconnect)weakCanReconnect = canReconnect; SRLogLPDebug(@"will reconnect in %@", self.reconnectDelay); - [[NSBlockOperation blockOperationWithBlock:^{ + //in order to force the retry timeout, we increase backoff for time + int isAliveDelay = MIN( [self.reconnectDelay integerValue] + pow(2.0, self.reconnectErrors), 360000);//NOTE: one hour will be much longer than connection's reconnect timeout. this is not indicating how long we wait to reconnect + self.reconnectTimeout = [NSBlockOperation blockOperationWithBlock:^{ __strong __typeof(&*weakSelf)strongSelf = weakSelf; __strong __typeof(&*weakConnection)strongConnection = weakConnection; - __strong __typeof(&*weakCanReconnect)strongCanReconnect = weakCanReconnect; SRLogLPWarn(@"reconnecting"); - [strongSelf connectionReconnect:strongConnection canReconnect:strongCanReconnect]; + [strongSelf connectionReconnect:strongConnection]; - }] performSelector:@selector(start) withObject:nil afterDelay:[self.reconnectDelay integerValue]]; + }]; + //note: we rely on our error and success callback to cancel this. + [self.reconnectTimeout performSelector:@selector(start) withObject:nil afterDelay:isAliveDelay]; } } -- (void)connectionReconnect:(id)connection canReconnect:(NSNumber *)canReconnect { - if ([canReconnect boolValue]) { - canReconnect = @(NO); - // Mark the connection as connected - if ([connection changeState:reconnecting toState:connected]) { - [connection didReconnect]; - } +- (void)connectionReconnect:(id)connection { + // Mark the connection as connected + if ([connection changeState:reconnecting toState:connected]) { + [connection didReconnect]; } } From 6550e5e205443b9356f6ae809df3b6eb6c79a798 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Wed, 12 Dec 2018 14:55:57 -0500 Subject: [PATCH 10/11] #300 only trigger reconnect on initialized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - left out an important change for 300: moving the reconnect logic exclusively to the message + initialized loop. prior code didn’t actually change anything --- SignalR.Client/Transports/SRServerSentEventsTransport.m | 7 ------- 1 file changed, 7 deletions(-) diff --git a/SignalR.Client/Transports/SRServerSentEventsTransport.m b/SignalR.Client/Transports/SRServerSentEventsTransport.m index bae78b98..d1f856b1 100644 --- a/SignalR.Client/Transports/SRServerSentEventsTransport.m +++ b/SignalR.Client/Transports/SRServerSentEventsTransport.m @@ -155,14 +155,7 @@ - (void)open:(id )connection connectionData:(NSString *)c //operation.securityPolicy = self.securityPolicy; _eventSource = [[SREventSourceStreamReader alloc] initWithStream:operation.outputStream]; _eventSource.opened = ^() { - __strong __typeof(&*weakConnection)strongConnection = weakConnection; SRLogSSEInfo(@"serverSentEvents did open eventSource"); - - // This will noop if we're not in the reconnecting state - if([strongConnection changeState:reconnecting toState:connected]) { - // Raise the reconnect event if the connection comes back up - [strongConnection didReconnect]; - } }; _eventSource.message = ^(SRServerSentEvent * sseEvent) { __strong __typeof(&*weakSelf)strongSelf = weakSelf; From f748a6cf2901a17ae9236559c7c2844e8c247d91 Mon Sep 17 00:00:00 2001 From: Joel Dart Date: Wed, 12 Dec 2018 15:22:20 -0500 Subject: [PATCH 11/11] fixup tests from #300 change --- Tests/Tests/SRServerSentEventsTransportTests.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Tests/SRServerSentEventsTransportTests.m b/Tests/Tests/SRServerSentEventsTransportTests.m index c77c204b..8d36c44f 100644 --- a/Tests/Tests/SRServerSentEventsTransportTests.m +++ b/Tests/Tests/SRServerSentEventsTransportTests.m @@ -199,7 +199,7 @@ - (void)testConnectionErrorRetries__RetriesAfterADelay__CommunicatesLifeCycleVia [NetworkMock stopMocking]; SRMockSSENetworkStream* NetworkReconnectMock = [[SRMockSSENetworkStream alloc]init]; [reconnectDelay.mock stopMocking];//dont want to accidentally get other blocks - [NetworkReconnectMock prepareForOpeningResponse:^{ + [NetworkReconnectMock prepareForOpeningResponse:@"data: initialized\n\n" then:^{ reconnectDelay.afterWait(); }]; @@ -259,7 +259,7 @@ - (void)testLostConnectionAbortsAllConnectionsAndReconnects { [NetworkMock stopMocking]; SRMockSSENetworkStream* NetworkReconnectMock = [[SRMockSSENetworkStream alloc]init]; [reconnectDelay.mock stopMocking];//dont want to accidentally get other blocks - [NetworkReconnectMock prepareForOpeningResponse:^{ + [NetworkReconnectMock prepareForOpeningResponse:@"data: initialized\n\n" then:^{ reconnectDelay.afterWait(); }]; @@ -855,7 +855,7 @@ - (void)testStreamClosesCleanlyShouldReconnect { [NetworkMock stopMocking]; SRMockSSENetworkStream* NetworkReconnectMock = [[SRMockSSENetworkStream alloc]init]; [reconnectDelay stopMocking]; - [NetworkReconnectMock prepareForOpeningResponse:^{ + [NetworkReconnectMock prepareForOpeningResponse:@"data: initialized\n\n" then:^{ reconnectDelay.afterWait(); }];