diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eaafd24 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +--- +language: objective-c + +before_script: + - sudo easy_install cpp-coveralls + +script: + - xctool project DTBonjour.xcodeproj -scheme "DTBonjour (iOS)" build -sdk iphonesimulator6.1 -arch i386 ONLY_ACTIVE_ARCH=NO + - xctool project DTBonjour.xcodeproj -scheme "DTBonjour (OS X)" build -arch x86_64 ONLY_ACTIVE_ARCH=NO + - appledoc -o /tmp . + +after_success: + - ./coveralls.rb --extension m --exclude-folder Demo --exclude-folder Test --exclude-folder Externals diff --git a/AppledocSettings.plist b/AppledocSettings.plist index 85d2e84..05430d2 100644 --- a/AppledocSettings.plist +++ b/AppledocSettings.plist @@ -30,6 +30,10 @@ --no-repeat-first-par + --include + + ./Documentation/Change Log-template.markdown + --ignore *.m @@ -37,16 +41,7 @@ Demo --index-desc - Readme.markdown - --include - - ./Documentation/Setup Guide-template.markdown - ./Documentation/Known Issues-template.markdown - ./Documentation/DTCoreText_Demo_App.png - ./Documentation/DTCoreText_Linker_Flags.png - ./Documentation/DTCoreText_Search_Paths.png - ./Documentation/DTCoreText_Reference.png - + readme.markdown --warn-invalid-crossref diff --git a/Core/Source/DTBonjourDataChunk.m b/Core/Source/DTBonjourDataChunk.m index 971df76..e706ee4 100644 --- a/Core/Source/DTBonjourDataChunk.m +++ b/Core/Source/DTBonjourDataChunk.m @@ -123,7 +123,7 @@ - (BOOL)_encodeObject:(id)object error:(NSError **)error { if (error) { - NSString *errorMsg = [NSString stringWithFormat:@"Unknown encoding type %d", _encoding]; + NSString *errorMsg = [NSString stringWithFormat:@"Unknown encoding type %d", (int)_encoding]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey:errorMsg}; *error = [NSError errorWithDomain:DTBonjourDataConnectionErrorDomain code:1 userInfo:userInfo]; } @@ -251,7 +251,7 @@ - (void)_decodeHeader */ _sequenceNumber = [headers[@"Sequence-Number:"] unsignedIntegerValue]; - _contentLength = [headers[@"Content-Length"] longLongValue]; + _contentLength = [[[NSNumberFormatter new] numberFromString:headers[@"Content-Length"]] unsignedIntegerValue]; _totalBytes = _rangeOfHeader.length + _contentLength; } diff --git a/Core/Source/DTBonjourDataConnection.h b/Core/Source/DTBonjourDataConnection.h index 3903970..07eb3e8 100644 --- a/Core/Source/DTBonjourDataConnection.h +++ b/Core/Source/DTBonjourDataConnection.h @@ -12,17 +12,30 @@ /** Type of encoding to use for sending objects */ -typedef enum +typedef NS_ENUM(NSUInteger, DTBonjourDataConnectionContentType) { + /** + Encode sent objects with NSCoding + */ DTBonjourDataConnectionContentTypeNSCoding = 0, + + /** + Encode sent objects as JSON. Note that not all kinds of Objective-C objects can be represented as JSON. + */ DTBonjourDataConnectionContentTypeJSON, -} DTBonjourDataConnectionContentType; +}; extern NSString * DTBonjourDataConnectionErrorDomain; +extern CGFloat DTBonjourDataConnectionDefaultTimeout; @class DTBonjourDataConnection, DTBonjourDataChunk; + +/** + Protocol to inform delegate of a DTBonjourDataConnection about what is happening + */ @protocol DTBonjourDataConnectionDelegate + @optional // sending @@ -130,11 +143,19 @@ extern NSString * DTBonjourDataConnectionErrorDomain; */ /** - Opens the connection and establishes the input and output streams. + Opens the connection and establishes the input and output streams. Cancels the + opening after a timeout of `DTBonjourDataConnectionDefaultTimeout` seconds. @returns `YES` if the connection could be established. */ - (BOOL)open; +/** + Opens the connection and establishes the input and output streams. + @param timeout Timeout in seconds after which to cancel the stream opening. + @returns `YES` if the connection could be established. + */ +- (BOOL)openWithTimeout:(CGFloat)timeout; + /** Closes the connection */ diff --git a/Core/Source/DTBonjourDataConnection.m b/Core/Source/DTBonjourDataConnection.m index 1a0a423..c615ae1 100644 --- a/Core/Source/DTBonjourDataConnection.m +++ b/Core/Source/DTBonjourDataConnection.m @@ -12,8 +12,88 @@ #import +#define kDTBonjourQNetworkAdditionsCheckSEL NSSelectorFromString(@"netService:didAcceptConnectionWithInputStream:outputStream:") + +CGFloat DTBonjourDataConnectionDefaultTimeout = 60.0; NSString * DTBonjourDataConnectionErrorDomain = @"DTBonjourDataConnection"; +@interface NSNetService (QNetworkAdditions) + +- (BOOL)qNetworkAdditions_getInputStream:(out NSInputStream **)inputStreamPtr + outputStream:(out NSOutputStream **)outputStreamPtr; + +@end + +@implementation NSNetService (QNetworkAdditions) + +- (BOOL)qNetworkAdditions_getInputStream:(out NSInputStream **)inputStreamPtr + outputStream:(out NSOutputStream **)outputStreamPtr + // The following works around three problems with + // -[NSNetService getInputStream:outputStream:]: + // + // o -- Currently the returns the streams with + // +1 retain count, which is counter to Cocoa conventions and results in + // leaks when you use it in ARC code. + // + // o -- If you create two pairs of streams from + // one NSNetService and then attempt to open all the streams simultaneously, + // some of the streams might fail to open. + // + // o -- If you create streams using + // -[NSNetService getInputStream:outputStream:], start to open them, and + // then release the last reference to the original NSNetService, the + // streams never finish opening. This problem is exacerbated under ARC + // because ARC is better about keeping things out of the autorelease pool. +{ + BOOL result; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; + + result = NO; + + readStream = NULL; + writeStream = NULL; + + if ( (inputStreamPtr != NULL) || (outputStreamPtr != NULL) ) { + CFNetServiceRef netService; + + netService = CFNetServiceCreate( + NULL, + (__bridge CFStringRef) [self domain], + (__bridge CFStringRef) [self type], + (__bridge CFStringRef) [self name], + 0 + ); + if (netService != NULL) { + CFStreamCreatePairWithSocketToNetService( + NULL, + netService, + ((inputStreamPtr != nil) ? &readStream : NULL), + ((outputStreamPtr != nil) ? &writeStream : NULL) + ); + CFRelease(netService); + } + + // We have failed if the client requested an input stream and didn't + // get one, or requested an output stream and didn't get one. We also + // fail if the client requested neither the input nor the output + // stream, but we don't get here in that case. + + result = ! ((( inputStreamPtr != NULL) && ( readStream == NULL)) || + ((outputStreamPtr != NULL) && (writeStream == NULL))); + } + if (inputStreamPtr != NULL) { + *inputStreamPtr = CFBridgingRelease(readStream); + } + if (outputStreamPtr != NULL) { + *outputStreamPtr = CFBridgingRelease(writeStream); + } + + return result; +} + +@end + @interface DTBonjourDataConnection () @end @@ -82,11 +162,27 @@ - (id)initWithService:(NSNetService *)service if (self) { - if (![service getInputStream:&_inputStream outputStream:&_outputStream]) - { - return nil; - } - + NSInputStream *in; + NSOutputStream *out; + + if (![[service delegate] respondsToSelector:kDTBonjourQNetworkAdditionsCheckSEL]) + { + // Older iOS/OSX versions need a patch for getting input and output + // streams see the `QNetworkAdditions` above. (If the delegate does not + // implement the `kDTBonjourQNetworkAdditionsCheck` selector, we can + // simply use the patched version. + if ([service qNetworkAdditions_getInputStream:&in outputStream:&out]) + return nil; + } + else + { + // iOS7/OSX10.9 + if (![service getInputStream:&in outputStream:&out]) + return nil; + } + + _inputStream = in; + _outputStream = out; _outputQueue = [[NSMutableArray alloc] init]; } @@ -113,7 +209,7 @@ - (void)dealloc [self close]; } -- (BOOL)open +- (BOOL)openWithTimeout:(CGFloat)timeout { [_inputStream setDelegate:self]; [_outputStream setDelegate:self]; @@ -122,9 +218,24 @@ - (BOOL)open [_inputStream open]; [_outputStream open]; + __weak id weakSelf = self; + double delayInSeconds = timeout; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + // No connection after timeout, closing. + if (![weakSelf isOpen]) { + [weakSelf close]; + } + }); + return YES; } +- (BOOL)open +{ + return [self openWithTimeout:DTBonjourDataConnectionDefaultTimeout]; +} + - (void)close { if (!_inputStream&&!_outputStream) @@ -142,50 +253,23 @@ - (void)close _outputStream = nil; if ([_delegate respondsToSelector:@selector(connectionDidClose:)]) - { [_delegate connectionDidClose:self]; - } } - (BOOL)isOpen { if (!_inputStream) - { return NO; - } NSStreamStatus inputStatus = [_inputStream streamStatus]; - - switch (inputStatus) - { - case NSStreamStatusNotOpen: - case NSStreamStatusAtEnd: - case NSStreamStatusClosed: - case NSStreamStatusError: - { - return NO; - } - - default: - break; - } - - NSStreamStatus outputStatus = [_outputStream streamStatus]; - - switch (outputStatus) - { - case NSStreamStatusNotOpen: - case NSStreamStatusAtEnd: - case NSStreamStatusClosed: - case NSStreamStatusError: - { - return NO; - } - - default: - break; - } - + NSStreamStatus outputStatus = [_outputStream streamStatus]; + + if (NSStreamStatusOpen != inputStatus) + return NO; + + if (NSStreamStatusOpen != outputStatus) + return NO; + return YES; } @@ -198,7 +282,7 @@ - (void)_startOutput DTBonjourDataChunk *chunk = _outputQueue[0]; - if (chunk.numberOfTransferredBytes==0) + if (0 == chunk.numberOfTransferredBytes) { // nothing sent yet if ([_delegate respondsToSelector:@selector(connection:willStartSendingChunk:)]) @@ -207,7 +291,7 @@ - (void)_startOutput } } - NSUInteger writtenBytes = [chunk writeToOutputStream:_outputStream]; + NSInteger writtenBytes = [chunk writeToOutputStream:_outputStream]; if (writtenBytes > 0) { @@ -252,12 +336,13 @@ - (BOOL)sendObject:(id)object error:(NSError **)error return NO; } - DTBonjourDataChunk *newChunk = [[DTBonjourDataChunk alloc] initWithObject:object encoding:self.sendingContentType error:error]; + DTBonjourDataChunk *newChunk = [[DTBonjourDataChunk alloc] + initWithObject:object + encoding:self.sendingContentType + error:error]; if (!newChunk) - { return NO; - } newChunk.sequenceNumber = _chunkSequenceNumber; @@ -267,7 +352,9 @@ - (BOOL)sendObject:(id)object error:(NSError **)error if (queueWasEmpty && _outputStream.streamStatus == NSStreamStatusOpen) { - [self _startOutput]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self _startOutput]; + }); } return YES; @@ -341,6 +428,8 @@ - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)streamEvent case NSStreamEventErrorOccurred: { NSLog(@"Error occurred: %@", [aStream.streamError localizedDescription]); + + // Intentional fall-through. } case NSStreamEventEndEncountered: diff --git a/DTBonjour.podspec b/DTBonjour.podspec index 39415a5..5c230c7 100644 --- a/DTBonjour.podspec +++ b/DTBonjour.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'DTBonjour' - spec.version = '1.0.0' + spec.version = '1.1.0' spec.summary = "Client/Server Communication of NSObjects over WiFi." spec.homepage = "https://github.com/Cocoanetics/DTBonjour" spec.author = { "Oliver Drobnik" => "oliver@drobnik.com" } diff --git a/DTBonjour.xcodeproj/project.pbxproj b/DTBonjour.xcodeproj/project.pbxproj index 336a13c..a66e993 100644 --- a/DTBonjour.xcodeproj/project.pbxproj +++ b/DTBonjour.xcodeproj/project.pbxproj @@ -330,6 +330,7 @@ A719022816A7F5E300C1EC52 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; DSTROOT = /tmp/DTBonjour.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Core/DTBonjour-Prefix.pch"; @@ -340,6 +341,7 @@ A719022916A7F5E300C1EC52 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = "$(ARCHS_STANDARD_INCLUDING_64_BIT)"; DSTROOT = /tmp/DTBonjour.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Core/DTBonjour-Prefix.pch"; diff --git a/DTBonjour.xcodeproj/xcshareddata/xcschemes/DTBonjour (OS X).xcscheme b/DTBonjour.xcodeproj/xcshareddata/xcschemes/DTBonjour (OS X).xcscheme new file mode 100644 index 0000000..48d1e16 --- /dev/null +++ b/DTBonjour.xcodeproj/xcshareddata/xcschemes/DTBonjour (OS X).xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DTBonjour.xcodeproj/xcshareddata/xcschemes/DTBonjour (iOS).xcscheme b/DTBonjour.xcodeproj/xcshareddata/xcschemes/DTBonjour (iOS).xcscheme new file mode 100644 index 0000000..1a543a1 --- /dev/null +++ b/DTBonjour.xcodeproj/xcshareddata/xcschemes/DTBonjour (iOS).xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Documentation/Change Log-template.markdown b/Documentation/Change Log-template.markdown new file mode 100644 index 0000000..bbff6a8 --- /dev/null +++ b/Documentation/Change Log-template.markdown @@ -0,0 +1,17 @@ +Change Log +========== + +This is the history of version updates. + +**Version 1.1.0** + +- FIXED: Various type warnings +- ADDED: New method on DTBonjourDataConnection which allows specifying a timeout +- ADDED: Delegate method for when connection was opened +- ADDED: arm64 support +- CHANGED: Updated for building with Xcode 5 +- CHANGED: Use original NSNetService method to get connection where workaround no longer necessary + +**Version 1.0.0** + +Initial release \ No newline at end of file diff --git a/coveralls.rb b/coveralls.rb new file mode 100755 index 0000000..e182aab --- /dev/null +++ b/coveralls.rb @@ -0,0 +1,136 @@ +#!/usr/bin/env ruby + +require 'etc' +require 'fileutils' +require 'find' +require 'optparse' + +# arraw of source subfolders to exclude +excludedFolders = [] +extensionsToProcess = [] +coveralls_cmd = "coveralls" + +excludeHeaders = false + +# create option parser +opts = OptionParser.new +opts.banner = "Usage: coveralls.rb [options]" + +opts.on('-e', '--exclude-folder FOLDER', 'Folder to exclude') do |v| + excludedFolders << v + coveralls_cmd.concat(" -e #{v}") +end + +opts.on('-h', '--exclude-headers', 'Ignores headers') do |v| + excludeHeaders = true +end + +opts.on('-x', '--extension EXT', 'Source file extension to process') do |v| + extensionsToProcess << v + coveralls_cmd.concat(" -x #{v}") +end + +opts.on_tail("-?", "--help", "Show this message") do + puts opts + exit +end + +# parse the options +begin + opts.parse!(ARGV) +rescue OptionParser::InvalidOption => e + puts e + puts opts + exit(1) +end + +# the folders +workingDir = Dir.getwd +derivedDataDir = "#{Etc.getpwuid.dir}/Library/Developer/Xcode/DerivedData/" +outputDir = workingDir + "/gcov" + +# create gcov output folder +FileUtils.mkdir outputDir + +# pattern to get source file from first line of gcov file +GCOV_SOURCE_PATTERN = Regexp.new(/Source:(.*)/) + +# enumerate all gcda files underneath derivedData +Find.find(derivedDataDir) do |gcda_file| + + if gcda_file.match(/\.gcda\Z/) + + #get just the folder name + gcov_dir = File.dirname(gcda_file) + + # cut off absolute working dir to get relative source path + relative_input_path = gcda_file.slice(derivedDataDir.length, gcda_file.length) + puts "\nINPUT: #{relative_input_path}" + + #process the file + result = %x( gcov '#{gcda_file}' -o '#{gcov_dir}' ) + + # filter the resulting output + Dir.glob("*.gcov") do |gcov_file| + + firstLine = File.open(gcov_file).readline + match = GCOV_SOURCE_PATTERN.match(firstLine) + + if (match) + + source_path = match[1] + + puts "source: #{source_path} - #{workingDir}" + + if (source_path.start_with? workingDir) + + # cut off absolute working dir to get relative source path + relative_path = source_path.slice(workingDir.length+1, source_path.length) + + extension = File.extname(relative_path) + extension = extension.slice(1, extension.length-1) + + puts "#{extension}" + + # get the path components + path_comps = relative_path.split(File::SEPARATOR) + + shouldProcess = false + exclusionMsg ="" + + if (excludedFolders.include?(path_comps[0])) + exclusionMsg = "excluded via option" + else + if (excludeHeaders == true && extension == 'h') + exclusionMsg = "excluded header" + else + if (extensionsToProcess.count == 0 || extensionsToProcess.include?(extension)) + shouldProcess = true + else + exclusionMsg = "excluded extension" + shouldProcess = false + end + end + end + + if (shouldProcess) + puts " - process: #{relative_path}" + FileUtils.mv(gcov_file, outputDir) + else + puts " - ignore: #{relative_path} (#{exclusionMsg})" + FileUtils.rm gcov_file + end + else + puts " - ignore: #{gcov_file} (outside source folder)" + FileUtils.rm gcov_file + end + end + end + end +end + +#call the coveralls, exclude some files +system coveralls_cmd + +#clean up +FileUtils.rm_rf outputDir diff --git a/readme.markdown b/readme.markdown index 63f9e31..032a84b 100644 --- a/readme.markdown +++ b/readme.markdown @@ -1,6 +1,8 @@ About DTBonjour ================== +[![Build Status](https://travis-ci.org/Cocoanetics/DTBonjour.png?branch=develop)](https://travis-ci.org/Cocoanetics/DTFoundation) [![Coverage Status](https://coveralls.io/repos/Cocoanetics/DTFoundation/badge.png?branch=develop)](https://coveralls.io/r/Cocoanetics/DTBonjour?branch=develop) + DTBonjour had its origin when I wanted communicate between a Mac app and an iOS app. It greatly simplifies networking over WiFi by giving you an easy method to transmit any NSObject that conforms to NSCoding. Documentation