Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

The return of TDSocketChangeTracker

* Fixes #177: Using it instead of TDConnectionChangeTracker to get around running out of sockets with 5+ replications active at once. (Also fixes #124 since the hostname-munging workaround is no longer needed.)
* It now uses CFReadStreamCreateForHTTPRequest.
* It now supports one-shot and long-poll modes, not just continuous.
  • Loading branch information...
commit c2fc5fea62e56247f60b94aeaf7a51a26bf96658 1 parent a4e35e5
@snej snej authored
View
8 Source/ChangeTracker/TDChangeTracker.h
@@ -28,12 +28,12 @@
typedef enum TDChangeTrackerMode {
kOneShot,
- kLongPoll
+ kLongPoll,
+ kContinuous
} TDChangeTrackerMode;
-/** Reads the continuous-mode _changes feed of a database, and sends the individual change entries to its client's -changeTrackerReceivedChange:.
- This class is abstract. Instantiate TDConnectionChangeTracker instead. */
+/** Reads the continuous-mode _changes feed of a database, and sends the individual change entries to its client. */
@interface TDChangeTracker : NSObject <NSStreamDelegate>
{
@protected
@@ -86,6 +86,8 @@ typedef enum TDChangeTrackerMode {
- (void) setUpstreamError: (NSString*)message;
- (void) failedWithError: (NSError*)error;
- (NSInteger) receivedPollResponse: (NSData*)body errorMessage: (NSString**)errorMessage;
+- (BOOL) receivedChanges: (NSArray*)changes errorMessage: (NSString**)errorMessage;
+- (BOOL) receivedChange: (NSDictionary*)change;
- (void) stopped; // override this
@end
View
110 Source/ChangeTracker/TDChangeTracker.m
@@ -16,7 +16,7 @@
// <http://wiki.apache.org/couchdb/HTTP_database_API#Changes>
#import "TDChangeTracker.h"
-#import "TDConnectionChangeTracker.h"
+#import "TDSocketChangeTracker.h"
#import "TDAuthorizer.h"
#import "TDMisc.h"
#import "TDStatus.h"
@@ -28,9 +28,6 @@
#define kMaxRetryDelay 300.0 // ...but will never get longer than this
-static NSURL* AddDotToURLHost( NSURL* url );
-
-
@interface TDChangeTracker ()
@property (readwrite, copy, nonatomic) id lastSequenceID;
@end
@@ -50,9 +47,17 @@ - (id)initWithDatabaseURL: (NSURL*)databaseURL
client: (id<TDChangeTrackerClient>)client {
NSParameterAssert(databaseURL);
NSParameterAssert(client);
- Assert([self class] != [TDChangeTracker class]); // abstract!
self = [super init];
if (self) {
+ if([self class] == [TDChangeTracker class]) {
+ // TDChangeTracker is abstract; instantiate a concrete subclass instead.
+ [self release];
+ return [[TDSocketChangeTracker alloc] initWithDatabaseURL: databaseURL
+ mode: mode
+ conflicts: includeConflicts
+ lastSequence: lastSequenceID
+ client: client];
+ }
_databaseURL = [databaseURL retain];
_client = client;
_mode = mode;
@@ -91,13 +96,7 @@ - (NSString*) changesFeedPath {
}
- (NSURL*) changesFeedURL {
- // Really ugly workaround for CFNetwork, to make sure that long-running connections like these
- // don't end up using the same socket pool as regular connections to the same host; otherwise
- // the regular connections can get stuck indefinitely behind a long-running one.
- // (This substitution appends a "." to the host name, if it didn't already end with one.)
- NSURL* url = AddDotToURLHost(_databaseURL);
-
- NSMutableString* urlStr = [[url.absoluteString mutableCopy] autorelease];
+ NSMutableString* urlStr = [[_databaseURL.absoluteString mutableCopy] autorelease];
if (![urlStr hasSuffix: @"/"])
[urlStr appendString: @"/"];
[urlStr appendString: self.changesFeedPath];
@@ -185,6 +184,27 @@ - (BOOL) receivedChange: (NSDictionary*)change {
return YES;
}
+- (BOOL) receivedChanges: (NSArray*)changes errorMessage: (NSString**)errorMessage {
+ if ([_client respondsToSelector: @selector(changeTrackerReceivedChanges:)]) {
+ [_client changeTrackerReceivedChanges: changes];
+ if (changes.count > 0)
+ self.lastSequenceID = [[changes lastObject] objectForKey: @"seq"];
+ } else {
+ for (NSDictionary* change in changes) {
+ if (![self receivedChange: change]) {
+ if (errorMessage) {
+ *errorMessage = $sprintf(@"Invalid change object: %@",
+ [TDJSON stringWithJSONObject: change
+ options:TDJSONWritingAllowFragments
+ error: nil]);
+ }
+ return NO;
+ }
+ }
+ }
+ return YES;
+}
+
- (NSInteger) receivedPollResponse: (NSData*)body errorMessage: (NSString**)errorMessage {
if (!body) {
*errorMessage = @"No body in response";
@@ -202,71 +222,9 @@ - (NSInteger) receivedPollResponse: (NSData*)body errorMessage: (NSString**)erro
*errorMessage = @"No 'changes' array in response";
return -1;
}
-
- if ([_client respondsToSelector: @selector(changeTrackerReceivedChanges:)]) {
- [_client changeTrackerReceivedChanges: changes];
- if (changes.count > 0)
- self.lastSequenceID = [[changes lastObject] objectForKey: @"seq"];
- } else {
- for (NSDictionary* change in changes) {
- if (![self receivedChange: change]) {
- *errorMessage = $sprintf(@"Invalid change object: %@",
- [TDJSON stringWithJSONObject: change
- options:TDJSONWritingAllowFragments
- error: nil]);
- return -1;
- }
- }
- }
+ if (![self receivedChanges: changes errorMessage: errorMessage])
+ return -1;
return changes.count;
}
@end
-
-
-static NSURL* AddDotToURLHost( NSURL* url ) {
- CAssert(url);
- UInt8 urlBytes[1024];
- CFIndex nBytes = CFURLGetBytes((CFURLRef)url, urlBytes, sizeof(urlBytes) - 1);
- if (nBytes > 0) {
- CFRange range;
- CFURLGetByteRangeForComponent((CFURLRef)url, kCFURLComponentHost, &range);
- if (range.length >= 2) {
- CFIndex end = range.location + range.length - 1;
- if (urlBytes[end] == '/' || urlBytes[end] == ':')
- --end;
- if (isalpha(urlBytes[end])) {
- // Alright, insert the '.' after end:
- memmove(&urlBytes[end+2], &urlBytes[end+1], nBytes - end);
- urlBytes[end+1] = '.';
- NSURL* newURL = (id)(CFURLCreateWithBytes(NULL, urlBytes, nBytes + 1,
- kCFStringEncodingUTF8, NULL));
- if (newURL)
- url = [newURL autorelease];
- else
- Warn(@"AddDotToURLHost: Failed to add dot to <%@> -- result is <%.*s>",
- url, (int)nBytes+1, urlBytes);
- }
- }
- }
- return url;
-}
-
-
-#if DEBUG
-static NSString* addDot( NSString* urlStr ) {
- return AddDotToURLHost([NSURL URLWithString: urlStr]).absoluteString;
-}
-
-TestCase(AddDotToURLHost) {
- CAssertEqual(addDot(@"http://x/y"), @"http://x./y");
- CAssertEqual(addDot(@"http://foo.com"), @"http://foo.com.");
- CAssertEqual(addDot(@"http://foo.com/"), @"http://foo.com./");
- CAssertEqual(addDot(@"http://foo.com/bar"), @"http://foo.com./bar");
- CAssertEqual(addDot(@"http://foo.com:123/"), @"http://foo.com.:123/");
- CAssertEqual(addDot(@"http://user:pass@foo.com/"), @"http://user:pass@foo.com./");
- CAssertEqual(addDot(@"http://foo.com./"), @"http://foo.com./");
- CAssertEqual(addDot(@"http://localhost/"), @"http://localhost./");
- CAssertEqual(addDot(@"http://10.0.1.12/"), @"http://10.0.1.12/");
-}
-#endif
View
59 Source/ChangeTracker/TDConnectionChangeTracker.m
@@ -23,17 +23,26 @@
#import "MYURLUtils.h"
+static NSURL* AddDotToURLHost( NSURL* url );
static SecTrustRef CopyTrustWithPolicy(SecTrustRef trust, SecPolicyRef policy);
@implementation TDConnectionChangeTracker
+- (NSURL*) changesFeedURL {
+ // Really ugly workaround for CFNetwork, to make sure that long-running connections like these
+ // don't end up using the same socket pool as regular connections to the same host; otherwise
+ // the regular connections can get stuck indefinitely behind a long-running one.
+ // (This substitution appends a "." to the host name, if it didn't already end with one.)
+ return AddDotToURLHost([super changesFeedURL]);
+}
+
- (BOOL) start {
if(_connection)
return NO;
[super start];
_inputBuffer = [[NSMutableData alloc] init];
-
+
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: self.changesFeedURL];
request.cachePolicy = NSURLRequestReloadIgnoringCacheData;
request.timeoutInterval = 6.02e23;
@@ -291,3 +300,51 @@ static SecTrustRef CopyTrustWithPolicy(SecTrustRef trust, SecPolicyRef policy) {
return trust;
#endif
}
+
+
+static NSURL* AddDotToURLHost( NSURL* url ) {
+ CAssert(url);
+ UInt8 urlBytes[1024];
+ CFIndex nBytes = CFURLGetBytes((CFURLRef)url, urlBytes, sizeof(urlBytes) - 1);
+ if (nBytes > 0) {
+ CFRange range;
+ CFURLGetByteRangeForComponent((CFURLRef)url, kCFURLComponentHost, &range);
+ if (range.length >= 2) {
+ CFIndex end = range.location + range.length - 1;
+ if (urlBytes[end] == '/' || urlBytes[end] == ':')
+ --end;
+ if (isalpha(urlBytes[end])) {
+ // Alright, insert the '.' after end:
+ memmove(&urlBytes[end+2], &urlBytes[end+1], nBytes - end);
+ urlBytes[end+1] = '.';
+ NSURL* newURL = (id)(CFURLCreateWithBytes(NULL, urlBytes, nBytes + 1,
+ kCFStringEncodingUTF8, NULL));
+ if (newURL)
+ url = [newURL autorelease];
+ else
+ Warn(@"AddDotToURLHost: Failed to add dot to <%@> -- result is <%.*s>",
+ url, (int)nBytes+1, urlBytes);
+ }
+ }
+ }
+ return url;
+}
+
+
+#if DEBUG
+static NSString* addDot( NSString* urlStr ) {
+ return AddDotToURLHost([NSURL URLWithString: urlStr]).absoluteString;
+}
+
+TestCase(AddDotToURLHost) {
+ CAssertEqual(addDot(@"http://x/y"), @"http://x./y");
+ CAssertEqual(addDot(@"http://foo.com"), @"http://foo.com.");
+ CAssertEqual(addDot(@"http://foo.com/"), @"http://foo.com./");
+ CAssertEqual(addDot(@"http://foo.com/bar"), @"http://foo.com./bar");
+ CAssertEqual(addDot(@"http://foo.com:123/"), @"http://foo.com.:123/");
+ CAssertEqual(addDot(@"http://user:pass@foo.com/"), @"http://user:pass@foo.com./");
+ CAssertEqual(addDot(@"http://foo.com./"), @"http://foo.com./");
+ CAssertEqual(addDot(@"http://localhost/"), @"http://localhost./");
+ CAssertEqual(addDot(@"http://10.0.1.12/"), @"http://10.0.1.12/");
+}
+#endif
View
28 Source/ChangeTracker/TDSocketChangeTracker.h
@@ -0,0 +1,28 @@
+//
+// TDSocketChangeTracker.h
+// TouchDB
+//
+// Created by Jens Alfke on 12/2/11.
+// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
+//
+
+#import "TDChangeTracker.h"
+
+
+/** TDChangeTracker implementation that uses a raw TCP socket to read the chunk-mode HTTP response. */
+@interface TDSocketChangeTracker : TDChangeTracker
+{
+ @private
+ NSInputStream* _trackingInput;
+
+ NSMutableData* _inputBuffer;
+ NSMutableData* _changeBuffer;
+ CFHTTPMessageRef _unauthResponse;
+ NSURLCredential* _credential;
+ CFAbsoluteTime _startTime;
+ bool _gotResponseHeaders;
+ bool _parsing;
+ bool _inputAvailable;
+ bool _atEOF;
+}
+@end
View
451 Source/ChangeTracker/TDSocketChangeTracker.m
@@ -0,0 +1,451 @@
+//
+// TDSocketChangeTracker.m
+// TouchDB
+//
+// Created by Jens Alfke on 12/2/11.
+// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License. You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software distributed under the
+// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+// either express or implied. See the License for the specific language governing permissions
+// and limitations under the License.
+//
+// <http://wiki.apache.org/couchdb/HTTP_database_API#Changes>
+
+#import "TDSocketChangeTracker.h"
+#import "TDRemoteRequest.h"
+#import "TDAuthorizer.h"
+#import "TDStatus.h"
+#import "TDBase64.h"
+#import "MYBlockUtils.h"
+#import "MYURLUtils.h"
+#import <string.h>
+
+
+#define kMaxRetries 6
+#define kInitialRetryDelay 0.2
+#define kReadLength 8192u
+
+
+@implementation TDSocketChangeTracker
+
+
+- (BOOL) start {
+ NSAssert(!_trackingInput, @"Already started");
+
+ LogTo(ChangeTracker, @"%@: Starting...", self);
+ [super start];
+
+ CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"),
+ (CFURLRef)self.changesFeedURL,
+ kCFHTTPVersion1_1);
+ Assert(request);
+
+ // Add headers.
+ [self.requestHeaders enumerateKeysAndObjectsUsingBlock: ^(id key, id value, BOOL *stop) {
+ CFHTTPMessageSetHeaderFieldValue(request, (CFStringRef)key, (CFStringRef)value);
+ }];
+
+ if (_unauthResponse && _credential) {
+ NSString* password = _credential.password;
+ if (password) {
+ CFIndex unauthStatus = CFHTTPMessageGetResponseStatusCode(_unauthResponse);
+ Assert(CFHTTPMessageAddAuthentication(request, _unauthResponse,
+ (CFStringRef)_credential.user,
+ (CFStringRef)password,
+ kCFHTTPAuthenticationSchemeBasic,
+ unauthStatus == 407));
+ } else {
+ Warn(@"%@: Unable to get password of %@", self, _credential);
+ }
+ } else if (_authorizer) {
+ NSString* authHeader = [_authorizer authorizeURLRequest: nil forRealm: nil];
+ if (authHeader) {
+ CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Authorization"),
+ (CFStringRef)authHeader);
+ }
+ }
+
+ CFReadStreamRef cfInputStream = CFReadStreamCreateForHTTPRequest(NULL, request);
+ CFRelease(request);
+ if (!cfInputStream)
+ return NO;
+ CFReadStreamSetProperty(cfInputStream, kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanTrue);
+ if (_databaseURL.my_isHTTPS) {
+ // Enable SSL for this connection.
+ // Disable TLS 1.2 support because it breaks compatibility with some SSL servers;
+ // workaround taken from Apple technote TN2287:
+ // http://developer.apple.com/library/ios/#technotes/tn2287/
+ NSDictionary *settings = $dict({(id)kCFStreamSSLLevel,
+ @"kCFStreamSocketSecurityLevelTLSv1_0SSLv3"});
+ CFReadStreamSetProperty(cfInputStream,
+ kCFStreamPropertySSLSettings, (CFTypeRef)settings);
+ }
+
+ _gotResponseHeaders = _atEOF = _inputAvailable = _parsing = false;
+
+ _inputBuffer = [[NSMutableData alloc] initWithCapacity: kReadLength];
+
+ _trackingInput = (NSInputStream*)cfInputStream;
+ [_trackingInput setDelegate: self];
+ [_trackingInput scheduleInRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes];
+ [_trackingInput open];
+ _startTime = CFAbsoluteTimeGetCurrent();
+ LogTo(ChangeTracker, @"%@: Started... <%@>", self, self.changesFeedURL);
+ return YES;
+}
+
+
+- (void) clearConnection {
+ [_trackingInput close];
+ [_trackingInput release];
+ _trackingInput = nil;
+
+ [_inputBuffer release];
+ _inputBuffer = nil;
+ [_changeBuffer release];
+ _changeBuffer = nil;
+}
+
+
+- (void) stop {
+ [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(start)
+ object: nil]; // cancel pending retries
+ if (_trackingInput) {
+ LogTo(ChangeTracker, @"%@: stop", self);
+ [self clearConnection];
+ }
+ [super stop];
+}
+
+
+- (void)dealloc
+{
+ if (_unauthResponse) CFRelease(_unauthResponse);
+ [_credential release];
+ [super dealloc];
+}
+
+
+#pragma mark - SSL & AUTHORIZATION:
+
+
+- (BOOL) checkSSLCert {
+ SecTrustRef sslTrust = (SecTrustRef) CFReadStreamCopyProperty((CFReadStreamRef)_trackingInput,
+ kCFStreamPropertySSLPeerTrust);
+ if (sslTrust) {
+ CFURLRef cfURL = CFReadStreamCopyProperty((CFReadStreamRef)_trackingInput,
+ kCFStreamPropertyHTTPFinalURL);
+ BOOL trusted = [TDRemoteRequest checkTrust: sslTrust forHost: [(NSURL*)cfURL host]];
+ CFRelease(cfURL);
+ CFRelease(sslTrust);
+ if (!trusted) {
+ //TODO: This error could be made more precise
+ self.error = [NSError errorWithDomain: NSURLErrorDomain
+ code: NSURLErrorServerCertificateUntrusted
+ userInfo: nil];
+ return NO;
+ }
+ }
+ return YES;
+}
+
+
+- (NSURLCredential*) credentialForResponse: (CFHTTPMessageRef)response {
+ NSString* realm;
+ NSString* authenticationMethod;
+
+ // Basic & digest auth: http://www.ietf.org/rfc/rfc2617.txt
+ CFStringRef str = CFHTTPMessageCopyHeaderFieldValue(response, CFSTR("WWW-Authenticate"));
+ NSString* authHeader = [NSMakeCollectable(str) autorelease];
+ if (!authHeader)
+ return nil;
+
+ // Get the auth type:
+ if ([authHeader hasPrefix: @"Basic"])
+ authenticationMethod = NSURLAuthenticationMethodHTTPBasic;
+ else if ([authHeader hasPrefix: @"Digest"])
+ authenticationMethod = NSURLAuthenticationMethodHTTPDigest;
+ else
+ return nil;
+
+ // Get the realm:
+ NSRange r = [authHeader rangeOfString: @"realm=\""];
+ if (r.length == 0)
+ return nil;
+ NSUInteger start = NSMaxRange(r);
+ r = [authHeader rangeOfString: @"\"" options: 0
+ range: NSMakeRange(start, authHeader.length - start)];
+ if (r.length == 0)
+ return nil;
+ realm = [authHeader substringWithRange: NSMakeRange(start, r.location - start)];
+
+ NSURLCredential* cred;
+ cred = [_databaseURL my_credentialForRealm: realm authenticationMethod: authenticationMethod];
+ if (!cred.hasPassword)
+ cred = nil; // TODO: Add support for client certs
+ return cred;
+}
+
+
+- (BOOL) readResponseHeader {
+ CFHTTPMessageRef response;
+ response = (CFHTTPMessageRef) CFReadStreamCopyProperty((CFReadStreamRef)_trackingInput,
+ kCFStreamPropertyHTTPResponseHeader);
+ Assert(response);
+ _gotResponseHeaders = true;
+
+ // Handle authentication failure (401 or 407 status):
+ CFIndex status = CFHTTPMessageGetResponseStatusCode(response);
+ LogTo(ChangeTracker, @"%@ got status %ld", self, status);
+ if ((status == 401 || status == 407) && !_credential
+ && ![_requestHeaders objectForKey: @"Authorization"]) {
+ _credential = [[self credentialForResponse: response] retain];
+ LogTo(ChangeTracker, @"%@: Auth challenge; credential = %@", self, _credential);
+ if (_credential) {
+ // Recoverable auth failure -- try again with _credential:
+ _unauthResponse = response;
+ [self errorOccurred: TDStatusToNSError((TDStatus)status, self.changesFeedURL)];
+ return NO;
+ }
+ }
+
+ CFRelease(response);
+ if (status >= 300) {
+ self.error = TDStatusToNSError(status, self.changesFeedURL);
+ return NO;
+ }
+ _retryCount = 0;
+ return YES;
+}
+
+
+#pragma mark - REGULAR-MDOE PARSING:
+
+
+- (void) readEntireInput {
+ // After one-shot or longpoll response is complete, parse it as a single JSON document:
+ NSData* input = [_inputBuffer retain];
+ LogTo(ChangeTracker, @"%@: Got entire body, %u bytes", self, (unsigned)input.length);
+ BOOL restart = NO;
+ NSString* errorMessage = nil;
+ NSInteger numChanges = [self receivedPollResponse: input errorMessage: &errorMessage];
+ if (numChanges < 0) {
+ // Oops, unparseable response:
+ restart = [self checkInvalidResponse: input];
+ if (!restart)
+ [self setUpstreamError: errorMessage];
+ } else {
+ // Poll again if there was no error, and either we're in longpoll mode or it looks like we
+ // ran out of changes due to a _limit rather than because we hit the end.
+ restart = _mode == kLongPoll || numChanges == (NSInteger)_limit;
+ }
+ [input release];
+
+ [self clearConnection];
+
+ if (restart)
+ [self start]; // Next poll...
+ else
+ [self stopped];
+}
+
+
+- (BOOL) checkInvalidResponse: (NSData*)body {
+ NSString* bodyStr = [[body my_UTF8ToString] stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if (_mode == kLongPoll && $equal(bodyStr, @"{\"results\":[")) {
+ // Looks like the connection got closed by a proxy (like AWS' load balancer) before
+ // the server had an actual change to send.
+ NSTimeInterval elapsed = CFAbsoluteTimeGetCurrent() - _startTime;
+ Warn(@"%@: Longpoll connection closed (by proxy?) after %.1f sec", self, elapsed);
+ if (elapsed >= 30.0) {
+ self.heartbeat = MIN(_heartbeat, elapsed * 0.75);
+ return YES; // should restart connection
+ }
+ } else if (bodyStr) {
+ Warn(@"%@: Unparseable response:\n%@", self, bodyStr);
+ } else {
+ Warn(@"%@: Response is invalid UTF-8; as CP1252:\n%@", self,
+ [[[NSString alloc] initWithData: body encoding: NSWindowsCP1252StringEncoding] autorelease]);
+ }
+ return NO;
+}
+
+
+#pragma mark - CONTINUOUS-MODE PARSING:
+
+
+- (void) readLines {
+ Assert(_gotResponseHeaders && _mode==kContinuous);
+ NSMutableArray* changes = $marray();
+ const char* pos = _inputBuffer.bytes;
+ const char* end = pos + _inputBuffer.length;
+ while (pos < end && _inputBuffer) {
+ const char* eol = memchr(pos, '\n', end-pos);
+ if (!eol)
+ break; // Wait till we have a complete line
+ ptrdiff_t lineLength = eol - pos;
+ if (lineLength > 0)
+ [changes addObject: [NSData dataWithBytes: pos length: lineLength]];
+ pos = eol + 1;
+ }
+
+ // Remove the parsed lines:
+ [_inputBuffer replaceBytesInRange: NSMakeRange(0, pos - (const char*)_inputBuffer.bytes)
+ withBytes: NULL length: 0];
+
+ if (changes.count > 0)
+ [self asyncParseChangeLines: changes];
+}
+
+
+- (void) asyncParseChangeLines: (NSArray*)lines {
+ static NSOperationQueue* sParseQueue;
+ if (!sParseQueue)
+ sParseQueue = [[NSOperationQueue alloc] init];
+
+ LogTo(ChangeTracker, @"%@: Async parsing %u changes...", self, (unsigned)lines.count);
+ Assert(!_parsing);
+ _parsing = true;
+ NSThread* resultThread = [NSThread currentThread];
+ [sParseQueue addOperationWithBlock: ^{
+ // Parse on background thread:
+ NSMutableArray* parsedChanges = [NSMutableArray arrayWithCapacity: lines.count];
+ for (NSData* line in lines) {
+ id change = [TDJSON JSONObjectWithData: line options: 0 error: NULL];
+ if (!change) {
+ Warn(@"TDSocketChangeTracker received unparseable change line from server: %@", [line my_UTF8ToString]);
+ break;
+ }
+ [parsedChanges addObject: change];
+ }
+ MYOnThread(resultThread, ^{
+ // Process change lines on original thread:
+ Assert(_parsing);
+ _parsing = false;
+ if (!_trackingInput)
+ return;
+ LogTo(ChangeTracker, @"%@: Notifying %u changes...", self, (unsigned)parsedChanges.count);
+ if (![self receivedChanges: parsedChanges errorMessage: NULL]) {
+ [self setUpstreamError: @"Unparseable change line"];
+ [self stop];
+ }
+
+ // Read more data if there is any, or stop if stream's at EOF:
+ if (_inputAvailable)
+ [self readFromInput];
+ else if (_atEOF)
+ [self stop];
+ });
+ }];
+}
+
+
+- (BOOL) failUnparseable: (NSString*)line {
+ Warn(@"Couldn't parse line from _changes: %@", line);
+ [self setUpstreamError: @"Unparseable change line"];
+ [self stop];
+ return NO;
+}
+
+
+#pragma mark - STREAM HANDLING:
+
+
+- (void) readFromInput {
+ Assert(!_parsing);
+ Assert(_inputAvailable);
+ _inputAvailable = false;
+
+ uint8_t* buffer;
+ NSUInteger bufferLength;
+ NSInteger bytesRead;
+ if ([_trackingInput getBuffer: &buffer length: &bufferLength]) {
+ [_inputBuffer appendBytes: buffer length: bufferLength];
+ bytesRead = bufferLength;
+ } else {
+ uint8_t buffer[kReadLength];
+ bytesRead = [_trackingInput read: buffer maxLength: sizeof(buffer)];
+ if (bytesRead > 0)
+ [_inputBuffer appendBytes: buffer length: bytesRead];
+ }
+ LogTo(ChangeTracker, @"%@: read %ld bytes", self, (long)bytesRead);
+
+ if (_mode == kContinuous)
+ [self readLines];
+}
+
+
+- (void) errorOccurred: (NSError*)error {
+ LogTo(ChangeTracker, @"%@: ErrorOccurred: %@", self, error);
+ if (++_retryCount <= kMaxRetries) {
+ [self clearConnection];
+ NSTimeInterval retryDelay = kInitialRetryDelay * (1 << (_retryCount-1));
+ [self performSelector: @selector(start) withObject: nil afterDelay: retryDelay];
+ } else {
+ Warn(@"%@: Can't connect, giving up: %@", self, error);
+ [self stop];
+
+ // Map lower-level errors from CFStream to higher-level NSURLError ones:
+ if ($equal(error.domain, NSPOSIXErrorDomain)) {
+ if (error.code == ECONNREFUSED)
+ error = [NSError errorWithDomain: NSURLErrorDomain
+ code: NSURLErrorCannotConnectToHost
+ userInfo: error.userInfo];
+ }
+
+ self.error = error;
+ }
+}
+
+
+- (void) stream: (NSStream*)stream handleEvent: (NSStreamEvent)eventCode {
+ [[self retain] autorelease]; // Delegate calling -stop might otherwise dealloc me
+
+ switch (eventCode) {
+ case NSStreamEventHasBytesAvailable: {
+ LogTo(ChangeTracker, @"%@: HasBytesAvailable %@", self, stream);
+ if (!_gotResponseHeaders) {
+ if (![self checkSSLCert] || ![self readResponseHeader])
+ return;
+ }
+ _inputAvailable = true;
+ // If still chewing on last bytes, don't eat any more yet
+ if (!_parsing)
+ [self readFromInput];
+ break;
+ }
+
+ case NSStreamEventEndEncountered:
+ LogTo(ChangeTracker, @"%@: EndEncountered %@", self, stream);
+ _atEOF = true;
+ if (!_gotResponseHeaders || (_mode == kContinuous && _inputBuffer.length > 0)) {
+ [self errorOccurred: [NSError errorWithDomain: NSURLErrorDomain
+ code: NSURLErrorNetworkConnectionLost
+ userInfo: nil]];
+ break;
+ }
+ if (_mode == kContinuous) {
+ if (!_parsing)
+ [self stop];
+ } else {
+ [self readEntireInput];
+ }
+ break;
+
+ case NSStreamEventErrorOccurred:
+ [self errorOccurred: stream.streamError];
+ break;
+
+ default:
+ LogTo(ChangeTracker, @"%@: Event %lx on %@", self, (long)eventCode, stream);
+ break;
+ }
+}
+
+
+@end
View
2  Source/TDAuthorizer.m
@@ -94,6 +94,8 @@ - (NSString*) authorizeURLRequest: (NSMutableURLRequest*)request
forRealm: (NSString*)realm
{
// <http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-00>
+ if (!request)
+ return nil;
NSString* nonce = $sprintf(@"%.0f:%@", -[_issueTime timeIntervalSinceNow], TDCreateUUID());
NSURL* url = request.URL;
NSString* ext = @""; // not implemented yet
View
65 Source/TDChangeTracker_Tests.m
@@ -5,10 +5,6 @@
// Created by Jens Alfke on 5/11/12.
// Copyright (c) 2012 Couchbase, Inc. All rights reserved.
//
-//
-// Created by Jens Alfke on 12/7/11.
-// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
-//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
// except in compliance with the License. You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
@@ -17,7 +13,7 @@
// either express or implied. See the License for the specific language governing permissions
// and limitations under the License.
-#import "TDConnectionChangeTracker.h"
+#import "TDChangeTracker.h"
#import "TDInternal.h"
#import "Test.h"
#import "MYURLUtils.h"
@@ -100,24 +96,27 @@ static void addTemporaryCredential(NSURL* url, NSString* realm,
}
-TestCase(TDChangeTracker) {
- TDChangeTrackerTester* tester = [[[TDChangeTrackerTester alloc] init] autorelease];
- NSURL* url = [NSURL URLWithString: @"http://snej.iriscouch.com/tdpuller_test1"];
- TDChangeTracker* tracker = [[[TDConnectionChangeTracker alloc] initWithDatabaseURL: url mode: kOneShot conflicts: NO lastSequence: nil client: tester] autorelease];
- NSArray* expected = $array($dict({@"seq", @1},
- {@"id", @"foo"},
- {@"changes", $array($dict({@"rev", @"5-ca289aa53cbbf35a5f5c799b64b1f16f"}))}),
- $dict({@"seq", @2},
- {@"id", @"attach"},
- {@"changes", $array($dict({@"rev", @"1-a7e2aad2bc8084b9041433182e292d8e"}))}),
- $dict({@"seq", @5},
- {@"id", @"bar"},
- {@"changes", $array($dict({@"rev", @"1-16f4304cd5ad8779fb40cb6bbbed60f5"}))}),
- $dict({@"seq", @6},
- {@"id", @"08a5cb4cc83156401c85bbe40e0007de"},
- {@"deleted", $true},
- {@"changes", $array($dict({@"rev", @"3-cbdb323dec78588cfea63bf7bb5a246f"}))}) );
- [tester run: tracker expectingChanges: expected];
+TestCase(TDChangeTracker_Simple) {
+ for (TDChangeTrackerMode mode = kOneShot; mode <= kContinuous; ++mode) {
+ Log(@"Mode = %d ...", mode);
+ TDChangeTrackerTester* tester = [[[TDChangeTrackerTester alloc] init] autorelease];
+ NSURL* url = [NSURL URLWithString: @"http://snej.iriscouch.com/tdpuller_test1"];
+ TDChangeTracker* tracker = [[[TDChangeTracker alloc] initWithDatabaseURL: url mode: mode conflicts: NO lastSequence: nil client: tester] autorelease];
+ NSArray* expected = $array($dict({@"seq", @1},
+ {@"id", @"foo"},
+ {@"changes", $array($dict({@"rev", @"5-ca289aa53cbbf35a5f5c799b64b1f16f"}))}),
+ $dict({@"seq", @2},
+ {@"id", @"attach"},
+ {@"changes", $array($dict({@"rev", @"1-a7e2aad2bc8084b9041433182e292d8e"}))}),
+ $dict({@"seq", @5},
+ {@"id", @"bar"},
+ {@"changes", $array($dict({@"rev", @"1-16f4304cd5ad8779fb40cb6bbbed60f5"}))}),
+ $dict({@"seq", @6},
+ {@"id", @"08a5cb4cc83156401c85bbe40e0007de"},
+ {@"deleted", $true},
+ {@"changes", $array($dict({@"rev", @"3-cbdb323dec78588cfea63bf7bb5a246f"}))}) );
+ [tester run: tracker expectingChanges: expected];
+ }
}
@@ -125,7 +124,7 @@ static void addTemporaryCredential(NSURL* url, NSString* realm,
// The only difference here is the "https:" scheme in the URL.
TDChangeTrackerTester* tester = [[[TDChangeTrackerTester alloc] init] autorelease];
NSURL* url = [NSURL URLWithString: @"https://snej.iriscouch.com/tdpuller_test1"];
- TDChangeTracker* tracker = [[[TDConnectionChangeTracker alloc] initWithDatabaseURL: url mode: kOneShot conflicts: NO lastSequence: 0 client: tester] autorelease];
+ TDChangeTracker* tracker = [[[TDChangeTracker alloc] initWithDatabaseURL: url mode: kOneShot conflicts: NO lastSequence: 0 client: tester] autorelease];
NSArray* expected = $array($dict({@"seq", @1},
{@"id", @"foo"},
{@"changes", $array($dict({@"rev", @"5-ca289aa53cbbf35a5f5c799b64b1f16f"}))}),
@@ -149,7 +148,7 @@ static void addTemporaryCredential(NSURL* url, NSString* realm,
NSURL* url = [NSURL URLWithString: @"https://dummy@snej.iriscouch.com/tdpuller_test2_auth"];
addTemporaryCredential(url, @"snejdom", @"dummy", @"dummy");
- TDChangeTracker* tracker = [[[TDConnectionChangeTracker alloc] initWithDatabaseURL: url mode: kOneShot conflicts: NO lastSequence: 0 client: tester] autorelease];
+ TDChangeTracker* tracker = [[[TDChangeTracker alloc] initWithDatabaseURL: url mode: kOneShot conflicts: NO lastSequence: 0 client: tester] autorelease];
NSArray* expected = $array($dict({@"seq", @1},
{@"id", @"something"},
{@"changes", $array($dict({@"rev", @"1-967a00dff5e02add41819138abb3284d"}))}) );
@@ -157,15 +156,23 @@ static void addTemporaryCredential(NSURL* url, NSString* realm,
}
-#if 0 // This test takes 31 seconds to run, so let's leave it turned off normally
TestCase(TDChangeTracker_Retry) {
+#if 0 // This test takes 31 seconds to run, so let's leave it turned off normally
// Intentionally connect to a nonexistent server to see the retry logic.
TDChangeTrackerTester* tester = [[[TDChangeTrackerTester alloc] init] autorelease];
NSURL* url = [NSURL URLWithString: @"https://localhost:5999/db"];
- TDChangeTracker* tracker = [[[TDConnectionChangeTracker alloc] initWithDatabaseURL: url mode: kOneShot conflicts: NO lastSequence: 0 client: tester] autorelease];
- [tester run: tracker expectingError: [NSError errorWithDomain: NSURLErrorDomain code: -1004 userInfo: nil]];
-}
+ TDChangeTracker* tracker = [[[TDChangeTracker alloc] initWithDatabaseURL: url mode: kOneShot conflicts: NO lastSequence: 0 client: tester] autorelease];
+ [tester run: tracker expectingError: [NSError errorWithDomain: NSURLErrorDomain code: NSURLErrorCannotConnectToHost userInfo: nil]];
#endif
+}
+
+
+TestCase(TDChangeTracker) {
+ RequireTestCase(TDChangeTracker_Simple);
+ RequireTestCase(TDChangeTracker_SSL);
+ RequireTestCase(TDChangeTracker_Auth);
+ RequireTestCase(TDChangeTracker_Retry);
+}
#endif // DEBUG
View
12 Source/TDPuller.m
@@ -17,7 +17,7 @@
#import "TDDatabase+Insertion.h"
#import "TDDatabase+Replication.h"
#import <TouchDB/TDRevision.h>
-#import "TDConnectionChangeTracker.h"
+#import "TDChangeTracker.h"
#import "TDAuthorizer.h"
#import "TDBatcher.h"
#import "TDMultipartDownloader.h"
@@ -89,11 +89,11 @@ - (void) startChangeTracker {
TDChangeTrackerMode mode = (_continuous && _caughtUp) ? kLongPoll : kOneShot;
LogTo(SyncVerbose, @"%@ starting ChangeTracker: mode=%d, since=%@", self, mode, _lastSequence);
- _changeTracker = [[TDConnectionChangeTracker alloc] initWithDatabaseURL: _remote
- mode: mode
- conflicts: YES
- lastSequence: _lastSequence
- client: self];
+ _changeTracker = [[TDChangeTracker alloc] initWithDatabaseURL: _remote
+ mode: mode
+ conflicts: YES
+ lastSequence: _lastSequence
+ client: self];
// Limit the number of changes to return, so we can parse the feed in parts:
_changeTracker.limit = kChangesFeedLimit;
_changeTracker.filterName = _filterName;
View
16 TouchDB.xcodeproj/project.pbxproj
@@ -207,8 +207,6 @@
27B0B7801491E76200A817AD /* TDView_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B77F1491E73400A817AD /* TDView_Tests.m */; };
27B0B796149290AB00A817AD /* TDChangeTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 27B0B790149290AB00A817AD /* TDChangeTracker.h */; };
27B0B797149290AB00A817AD /* TDChangeTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B791149290AB00A817AD /* TDChangeTracker.m */; };
- 27B0B798149290AB00A817AD /* TDConnectionChangeTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 27B0B792149290AB00A817AD /* TDConnectionChangeTracker.h */; };
- 27B0B799149290AB00A817AD /* TDConnectionChangeTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B793149290AB00A817AD /* TDConnectionChangeTracker.m */; };
27B0B79E1492932800A817AD /* TDBase64.h in Headers */ = {isa = PBXBuildFile; fileRef = 27B0B79C1492932700A817AD /* TDBase64.h */; };
27B0B79F1492932800A817AD /* TDBase64.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B79D1492932700A817AD /* TDBase64.m */; };
27B0B7AA1492B83B00A817AD /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27F0745C11CD50A600E9A2AB /* Foundation.framework */; };
@@ -228,7 +226,6 @@
27B0B7D61492B8A200A817AD /* TDPuller.m in Sources */ = {isa = PBXBuildFile; fileRef = 270B3E2A1489581E00E0A926 /* TDPuller.m */; };
27B0B7D71492B8A200A817AD /* TDPusher.m in Sources */ = {isa = PBXBuildFile; fileRef = 270B3E3D148D7F0000E0A926 /* TDPusher.m */; };
27B0B7D81492B8A200A817AD /* TDChangeTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B791149290AB00A817AD /* TDChangeTracker.m */; };
- 27B0B7D91492B8A200A817AD /* TDConnectionChangeTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B793149290AB00A817AD /* TDConnectionChangeTracker.m */; };
27B0B7DB1492B8A200A817AD /* TDBase64.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B0B79D1492932700A817AD /* TDBase64.m */; };
27B0B7DC1492B8B200A817AD /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F0747011CD51A200E9A2AB /* FMDatabase.m */; };
27B0B7DD1492B8B200A817AD /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F0747211CD51A200E9A2AB /* FMResultSet.m */; };
@@ -305,6 +302,9 @@
27E7FAF1155D8ECE0025F93A /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E7FAEF155D8EBA0025F93A /* CFNetwork.framework */; };
27E7FB03155DBDA20025F93A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27E7FB02155DBDA20025F93A /* Security.framework */; };
27E7FB04155DBDBF0025F93A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 275315F414ACF1CC0065964D /* Security.framework */; };
+ 27ED9AA1163B01D5000C844A /* TDSocketChangeTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 27ED9AA0163B01D5000C844A /* TDSocketChangeTracker.h */; };
+ 27ED9AA3163B01E2000C844A /* TDSocketChangeTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 27ED9AA2163B01E2000C844A /* TDSocketChangeTracker.m */; };
+ 27ED9AA4163B01E2000C844A /* TDSocketChangeTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 27ED9AA2163B01E2000C844A /* TDSocketChangeTracker.m */; };
27F0744A11CD4BA000E9A2AB /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 27F0744911CD4BA000E9A2AB /* libsqlite3.dylib */; };
27F0745D11CD50A600E9A2AB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27F0745C11CD50A600E9A2AB /* Foundation.framework */; };
27F0747611CD51A200E9A2AB /* FMDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F0747011CD51A200E9A2AB /* FMDatabase.m */; };
@@ -667,6 +667,8 @@
27E7FAEA155D78C20025F93A /* TDChangeTracker_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TDChangeTracker_Tests.m; sourceTree = "<group>"; };
27E7FAEF155D8EBA0025F93A /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.1.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; };
27E7FB02155DBDA20025F93A /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.1.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; };
+ 27ED9AA0163B01D5000C844A /* TDSocketChangeTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TDSocketChangeTracker.h; sourceTree = "<group>"; };
+ 27ED9AA2163B01E2000C844A /* TDSocketChangeTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TDSocketChangeTracker.m; sourceTree = "<group>"; };
27F0744611CD4B6D00E9A2AB /* TDDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TDDatabase.h; sourceTree = "<group>"; };
27F0744711CD4B6D00E9A2AB /* TDDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TDDatabase.m; sourceTree = "<group>"; };
27F0744911CD4BA000E9A2AB /* libsqlite3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libsqlite3.dylib; path = usr/lib/libsqlite3.dylib; sourceTree = SDKROOT; };
@@ -1158,6 +1160,8 @@
27B0B791149290AB00A817AD /* TDChangeTracker.m */,
27B0B792149290AB00A817AD /* TDConnectionChangeTracker.h */,
27B0B793149290AB00A817AD /* TDConnectionChangeTracker.m */,
+ 27ED9AA0163B01D5000C844A /* TDSocketChangeTracker.h */,
+ 27ED9AA2163B01E2000C844A /* TDSocketChangeTracker.m */,
);
path = ChangeTracker;
sourceTree = "<group>";
@@ -1378,7 +1382,6 @@
279EB2D11491442500E74185 /* TDInternal.h in Headers */,
279EB2DB1491C34300E74185 /* TDCollateJSON.h in Headers */,
27B0B796149290AB00A817AD /* TDChangeTracker.h in Headers */,
- 27B0B798149290AB00A817AD /* TDConnectionChangeTracker.h in Headers */,
27B0B79E1492932800A817AD /* TDBase64.h in Headers */,
27731EFF1493FA3100815D67 /* TDBlobStore.h in Headers */,
279906E3149A65B8003D4338 /* TDRemoteRequest.h in Headers */,
@@ -1429,6 +1432,7 @@
270F5705156AE0BF000FEB8F /* TDAuthorizer.h in Headers */,
275F535015C04F7B00BAF578 /* MYStreamUtils.h in Headers */,
2781E3C915C31FDA00E970DC /* MYRegexUtils.h in Headers */,
+ 27ED9AA1163B01D5000C844A /* TDSocketChangeTracker.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1931,7 +1935,6 @@
279EB2DC1491C34300E74185 /* TDCollateJSON.m in Sources */,
27B0B7801491E76200A817AD /* TDView_Tests.m in Sources */,
27B0B797149290AB00A817AD /* TDChangeTracker.m in Sources */,
- 27B0B799149290AB00A817AD /* TDConnectionChangeTracker.m in Sources */,
27B0B79F1492932800A817AD /* TDBase64.m in Sources */,
27731F001493FA3100815D67 /* TDBlobStore.m in Sources */,
279906E5149A65B8003D4338 /* TDRemoteRequest.m in Sources */,
@@ -1976,6 +1979,7 @@
270F5706156AE0BF000FEB8F /* TDAuthorizer.m in Sources */,
275F535115C04F7B00BAF578 /* MYStreamUtils.m in Sources */,
2781E3CA15C31FDA00E970DC /* MYRegexUtils.m in Sources */,
+ 27ED9AA3163B01E2000C844A /* TDSocketChangeTracker.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2048,7 +2052,6 @@
27B0B7D61492B8A200A817AD /* TDPuller.m in Sources */,
27B0B7D71492B8A200A817AD /* TDPusher.m in Sources */,
27B0B7D81492B8A200A817AD /* TDChangeTracker.m in Sources */,
- 27B0B7D91492B8A200A817AD /* TDConnectionChangeTracker.m in Sources */,
27B0B7DB1492B8A200A817AD /* TDBase64.m in Sources */,
27B0B7DC1492B8B200A817AD /* FMDatabase.m in Sources */,
27B0B7DD1492B8B200A817AD /* FMResultSet.m in Sources */,
@@ -2094,6 +2097,7 @@
270F5707156AE0BF000FEB8F /* TDAuthorizer.m in Sources */,
275F535215C04F7B00BAF578 /* MYStreamUtils.m in Sources */,
2781E3CB15C31FDA00E970DC /* MYRegexUtils.m in Sources */,
+ 27ED9AA4163B01E2000C844A /* TDSocketChangeTracker.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Please sign in to comment.
Something went wrong with that request. Please try again.