diff --git a/Releases/WWDC_latest.zip b/Releases/WWDC_latest.zip index 374d869a..c9d21753 100644 Binary files a/Releases/WWDC_latest.zip and b/Releases/WWDC_latest.zip differ diff --git a/Updater/Updater.xcodeproj/project.pbxproj b/Updater/Updater.xcodeproj/project.pbxproj new file mode 100644 index 00000000..dff9e40b --- /dev/null +++ b/Updater/Updater.xcodeproj/project.pbxproj @@ -0,0 +1,342 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + DD89E9BF1AF425600073B845 /* Updater.h in Headers */ = {isa = PBXBuildFile; fileRef = DD89E9BE1AF4255F0073B845 /* Updater.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD89E9E01AF425B10073B845 /* UDUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = DD89E9DE1AF425B10073B845 /* UDUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD89E9E11AF425B10073B845 /* UDUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = DD89E9DF1AF425B10073B845 /* UDUpdater.m */; }; + DD89E9EA1AF42B360073B845 /* UDRelease.h in Headers */ = {isa = PBXBuildFile; fileRef = DD89E9E81AF42B360073B845 /* UDRelease.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DD89E9EB1AF42B360073B845 /* UDRelease.m in Sources */ = {isa = PBXBuildFile; fileRef = DD89E9E91AF42B360073B845 /* UDRelease.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DD89E9F01AF481F40073B845 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + DD89E9B91AF4255F0073B845 /* Updater.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Updater.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DD89E9BD1AF4255F0073B845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DD89E9BE1AF4255F0073B845 /* Updater.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Updater.h; sourceTree = ""; }; + DD89E9DE1AF425B10073B845 /* UDUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UDUpdater.h; sourceTree = ""; }; + DD89E9DF1AF425B10073B845 /* UDUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UDUpdater.m; sourceTree = ""; }; + DD89E9E81AF42B360073B845 /* UDRelease.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UDRelease.h; sourceTree = ""; }; + DD89E9E91AF42B360073B845 /* UDRelease.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UDRelease.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DD89E9B51AF4255F0073B845 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DD89E9AF1AF4255F0073B845 = { + isa = PBXGroup; + children = ( + DD89E9BB1AF4255F0073B845 /* Updater */, + DD89E9BA1AF4255F0073B845 /* Products */, + ); + sourceTree = ""; + }; + DD89E9BA1AF4255F0073B845 /* Products */ = { + isa = PBXGroup; + children = ( + DD89E9B91AF4255F0073B845 /* Updater.framework */, + ); + name = Products; + sourceTree = ""; + }; + DD89E9BB1AF4255F0073B845 /* Updater */ = { + isa = PBXGroup; + children = ( + DD89E9BE1AF4255F0073B845 /* Updater.h */, + DD89E9BC1AF4255F0073B845 /* Supporting Files */, + DD89E9DE1AF425B10073B845 /* UDUpdater.h */, + DD89E9DF1AF425B10073B845 /* UDUpdater.m */, + DD89E9E81AF42B360073B845 /* UDRelease.h */, + DD89E9E91AF42B360073B845 /* UDRelease.m */, + ); + path = Updater; + sourceTree = ""; + }; + DD89E9BC1AF4255F0073B845 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + DD89E9BD1AF4255F0073B845 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + DD89E9B61AF4255F0073B845 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DD89E9BF1AF425600073B845 /* Updater.h in Headers */, + DD89E9EA1AF42B360073B845 /* UDRelease.h in Headers */, + DD89E9E01AF425B10073B845 /* UDUpdater.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + DD89E9B81AF4255F0073B845 /* Updater */ = { + isa = PBXNativeTarget; + buildConfigurationList = DD89E9CF1AF425600073B845 /* Build configuration list for PBXNativeTarget "Updater" */; + buildPhases = ( + DD89E9B41AF4255F0073B845 /* Sources */, + DD89E9B51AF4255F0073B845 /* Frameworks */, + DD89E9B61AF4255F0073B845 /* Headers */, + DD89E9B71AF4255F0073B845 /* Resources */, + DD89E9F01AF481F40073B845 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Updater; + productName = Updater; + productReference = DD89E9B91AF4255F0073B845 /* Updater.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DD89E9B01AF4255F0073B845 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0630; + ORGANIZATIONNAME = "Guilherme Rambo"; + TargetAttributes = { + DD89E9B81AF4255F0073B845 = { + CreatedOnToolsVersion = 6.3.1; + }; + }; + }; + buildConfigurationList = DD89E9B31AF4255F0073B845 /* Build configuration list for PBXProject "Updater" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = DD89E9AF1AF4255F0073B845; + productRefGroup = DD89E9BA1AF4255F0073B845 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DD89E9B81AF4255F0073B845 /* Updater */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DD89E9B71AF4255F0073B845 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DD89E9B41AF4255F0073B845 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD89E9EB1AF42B360073B845 /* UDRelease.m in Sources */, + DD89E9E11AF425B10073B845 /* UDUpdater.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + DD89E9CD1AF425600073B845 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + DD89E9CE1AF425600073B845 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + DD89E9D01AF425600073B845 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Updater", + ); + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + ); + INFOPLIST_FILE = Updater/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Updater/vendor", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + DD89E9D11AF425600073B845 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Updater", + ); + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + ); + INFOPLIST_FILE = Updater/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Updater/vendor", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DD89E9B31AF4255F0073B845 /* Build configuration list for PBXProject "Updater" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD89E9CD1AF425600073B845 /* Debug */, + DD89E9CE1AF425600073B845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DD89E9CF1AF425600073B845 /* Build configuration list for PBXNativeTarget "Updater" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD89E9D01AF425600073B845 /* Debug */, + DD89E9D11AF425600073B845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; +/* End XCConfigurationList section */ + }; + rootObject = DD89E9B01AF4255F0073B845 /* Project object */; +} diff --git a/Updater/Updater/Info.plist b/Updater/Updater/Info.plist new file mode 100644 index 00000000..0ffe0b2f --- /dev/null +++ b/Updater/Updater/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + br.com.guilhermerambo.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2015 Guilherme Rambo. All rights reserved. + NSPrincipalClass + + + diff --git a/Updater/Updater/UDRelease.h b/Updater/Updater/UDRelease.h new file mode 100644 index 00000000..c97ac519 --- /dev/null +++ b/Updater/Updater/UDRelease.h @@ -0,0 +1,36 @@ +// +// UDRelease.h +// Updater +// +// Created by Guilherme Rambo on 01/05/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +@import Cocoa; + +@interface UDRelease : NSObject + +@property (assign) int identifier; + +@property (copy) NSString *version; +@property (copy) NSString *notes; +@property (copy) NSString *download; + +@property (readonly) NSURL *downloadURL; + +@property (readonly) int majorVersion; +@property (readonly) int minorVersion; +@property (readonly) int patchVersion; + +@property (assign) BOOL prerelease; +@property (assign) BOOL draft; + ++ (UDRelease *)releaseWithDictionaryRepresentation:(NSDictionary *)dict; + +@end + +@interface NSApplication (UDRelease) + +@property (readonly) UDRelease *ud_currentRelease; + +@end \ No newline at end of file diff --git a/Updater/Updater/UDRelease.m b/Updater/Updater/UDRelease.m new file mode 100644 index 00000000..afeed0a2 --- /dev/null +++ b/Updater/Updater/UDRelease.m @@ -0,0 +1,100 @@ +// +// UDRelease.m +// Updater +// +// Created by Guilherme Rambo on 01/05/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import "UDRelease.h" + +@implementation UDRelease + ++ (UDRelease *)releaseWithDictionaryRepresentation:(NSDictionary *)dict +{ + UDRelease *release = [[UDRelease alloc] init]; + + release.identifier = [dict[@"id"] intValue]; + release.version = dict[@"tag_name"]; + release.notes = dict[@"body"]; + release.download = dict[@"assets"][0][@"browser_download_url"]; + release.prerelease = [dict[@"prerelease"] boolValue]; + release.draft = [dict[@"draft"] boolValue]; + + return release; +} + +- (NSURL *)downloadURL +{ + return [NSURL URLWithString:self.download]; +} + +- (int)majorVersion +{ + NSArray *components = [self.version componentsSeparatedByString:@"."]; + + return [components[0] intValue]; +} + +- (int)minorVersion +{ + NSArray *components = [self.version componentsSeparatedByString:@"."]; + + if (components.count > 1) { + return [components[1] intValue]; + } else { + return 0; + } +} + +- (int)patchVersion +{ + NSArray *components = [self.version componentsSeparatedByString:@"."]; + + if (components.count > 2) { + return [components[2] intValue]; + } else { + return 0; + } +} + +- (BOOL)isGreaterThan:(id)object +{ + UDRelease *otherRelease = object; + + if (self.majorVersion > otherRelease.majorVersion) { + return YES; + } + + if (self.majorVersion == otherRelease.majorVersion) { + if (self.minorVersion == otherRelease.minorVersion) { + if (self.patchVersion > otherRelease.patchVersion) { + return YES; + } else { + return NO; + } + } else { + if (self.minorVersion > otherRelease.minorVersion) { + return YES; + } + } + } + + return NO; +} + +@end + + +@implementation NSApplication (UDRelease) + +- (UDRelease *)ud_currentRelease +{ + UDRelease *release = [[UDRelease alloc] init]; + + release.version = [NSBundle mainBundle].infoDictionary[@"CFBundleShortVersionString"]; + + return release; +} + +@end \ No newline at end of file diff --git a/Updater/Updater/UDUpdater.h b/Updater/Updater/UDUpdater.h new file mode 100644 index 00000000..1c80ca67 --- /dev/null +++ b/Updater/Updater/UDUpdater.h @@ -0,0 +1,34 @@ +// +// UDUpdater.h +// Updater +// +// Created by Guilherme Rambo on 01/05/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +@import Cocoa; + +@class UDRelease; + +@interface UDUpdater : NSObject + ++ (UDUpdater *)sharedUpdater; + +/** + If updateAutomatically is set to YES, updates are downloaded and applied automatically + */ +@property (nonatomic, assign) BOOL updateAutomatically; + +/** + Checks for new releases, if a new release is found, "latestRelease" contains information about It. + "callback" is executed on main thread + */ +- (void)checkForUpdatesWithCompletionHandler:(void (^)(UDRelease *latestRelease))callback; + +/** + Downloads and installs the specified release, this is only necessary if updateAutomatically is set to NO + "callback" is executed on main thread + */ +- (void)downloadAndInstallRelease:(UDRelease *)release completionHandler:(void(^)(BOOL installed))callback; + +@end diff --git a/Updater/Updater/UDUpdater.m b/Updater/Updater/UDUpdater.m new file mode 100644 index 00000000..8b56f4a6 --- /dev/null +++ b/Updater/Updater/UDUpdater.m @@ -0,0 +1,260 @@ +// +// UDUpdater.m +// Updater +// +// Created by Guilherme Rambo on 01/05/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import "UDUpdater.h" +#import "UDRelease.h" + +#define kGithubAPIEndpoint @"https://api.github.com/" +#define kRepoCreatorKey @"UDRepoCreator" +#define kRepoNameKey @"UDRepoName" + +@interface UDUpdater () + +@property (strong) NSURLSession *session; + +@property (readonly) NSString *repoCreator; +@property (readonly) NSString *repoName; +@property (readonly) NSURL *releasesURL; + +@property (readonly) NSString *releaseFilenamePrefix; +@property (readonly) NSString *localStoragePath; + +@end + +@implementation UDUpdater + ++ (UDUpdater *)sharedUpdater +{ + static UDUpdater *_updater; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _updater = [[UDUpdater alloc] init]; + }); + + return _updater; +} + +- (void)checkForUpdatesWithCompletionHandler:(void (^)(UDRelease *latestRelease))callback +{ + [[self.session dataTaskWithURL:self.releasesURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + #ifdef DEBUG + NSLog(@"Updater failed to get updates from %@. Error: %@", self.releasesURL, error); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil); + }); + return; + } + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode > 400) { + #ifdef DEBUG + NSLog(@"Updater failed to get updates from %@. Status code: %ld", self.releasesURL, (long)httpResponse.statusCode); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil); + }); + return; + } + + NSError *jsonError; + NSArray *releases = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError]; + if (jsonError) { + #ifdef DEBUG + NSLog(@"Updater failed to process JSON data from %@. Error: %@", self.releasesURL, jsonError); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil); + }); + return; + } + + UDRelease *latestRelease = [UDRelease releaseWithDictionaryRepresentation:releases[0]]; + #ifdef DEBUG + NSLog(@"Latest release available is %@", latestRelease.version); + #endif + + if (latestRelease.prerelease || latestRelease.draft) { + #ifdef DEBUG + NSLog(@"Latest release is prerelease or draft, ignoring."); + #endif + } + + if ([latestRelease isGreaterThan:[NSApplication sharedApplication].ud_currentRelease]) { + dispatch_async(dispatch_get_main_queue(), ^{ + callback(latestRelease); + }); + if (self.updateAutomatically) [self downloadAndInstallRelease:latestRelease completionHandler:nil]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + callback(nil); + }); + } + }] resume]; +} + +- (void)downloadAndInstallRelease:(UDRelease *)release completionHandler:(void (^)(BOOL))callback +{ + [[self.session downloadTaskWithURL:release.downloadURL completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + if (error) { + #ifdef DEBUG + NSLog(@"Updater failed to download release %@ from %@", release.version, release.downloadURL); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback) callback(NO); + }); + return; + } + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode > 400) { + #ifdef DEBUG + NSLog(@"Updater failed to download release from %@. Status code: %ld", self.releasesURL, (long)httpResponse.statusCode); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback) callback(NO); + }); + return; + } + + NSString *path = [self.localStoragePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@_%d.zip", self.releaseFilenamePrefix, release.version, release.identifier]]; + NSError *moveError; + [[NSFileManager defaultManager] moveItemAtPath:location.path toPath:path error:&moveError]; + + if (moveError) { + #ifdef DEBUG + NSLog(@"Updater failed to move downloaded release from %@ to %@", location.path, path); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback) callback(NO); + }); + return; + } else { + #ifdef DEBUG + NSLog(@"New release downloaded and moved from %@ to %@", location.path, path); + #endif + + [self installRelease:release fromFileAtPath:path completionHandler:callback]; + } + + }] resume]; +} + +#pragma mark Private API + +- (instancetype)init +{ + if (!(self = [super init])) return nil; + + self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + + return self; +} + +- (NSString *)repoCreator +{ + NSDictionary *dict = [[NSBundle mainBundle] infoDictionary]; + + return dict[kRepoCreatorKey]; +} + +- (NSString *)repoName +{ + NSDictionary *dict = [[NSBundle mainBundle] infoDictionary]; + + return dict[kRepoNameKey]; +} + +- (NSURL *)releasesURL +{ + NSString *path = [NSString pathWithComponents:@[@"repos", self.repoCreator, self.repoName, @"releases"]]; + NSString *urlString = [kGithubAPIEndpoint stringByAppendingPathComponent:path]; + + return [NSURL URLWithString:urlString]; +} + +- (NSString *)localStoragePath +{ + NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; + NSString *path = [cacheDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@temp%d", self.releaseFilenamePrefix, [NSProcessInfo processInfo].processIdentifier]]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:NULL]) { + [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:NULL]; + } + + return path; +} + +- (NSString *)releaseFilenamePrefix +{ + return [NSString stringWithFormat:@"%@_update_", [NSProcessInfo processInfo].processName]; +} + +- (void)installRelease:(UDRelease *)release fromFileAtPath:(NSString *)path completionHandler:(void(^)(BOOL success))callback +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + NSTask *unzipTask = [[NSTask alloc] init]; + unzipTask.launchPath = @"/usr/bin/unzip"; + unzipTask.currentDirectoryPath = path.stringByDeletingLastPathComponent; + unzipTask.arguments = @[path]; + unzipTask.standardError = [NSPipe pipe]; + unzipTask.standardOutput = [NSPipe pipe]; + [unzipTask launch]; + [unzipTask waitUntilExit]; + + // check unzip result + if (unzipTask.terminationStatus != 0) { + #ifdef DEBUG + NSLog(@"Failed to unzip %@", path); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback) callback(NO); + }); + return; + } + + // current running app's path + NSString *currentAppPath = [NSBundle mainBundle].bundlePath; + // downloaded and unzipped app's path + NSString *newAppPath = [self.localStoragePath stringByAppendingPathComponent:currentAppPath.lastPathComponent]; + + // swap current app with the downloaded app + NSError *deleteError; + if ([[NSFileManager defaultManager] removeItemAtPath:currentAppPath error:&deleteError]) { + NSError *moveError; + if ([[NSFileManager defaultManager] moveItemAtPath:newAppPath toPath:currentAppPath error:&moveError]) { + + // delete temporary directory + [[NSFileManager defaultManager] removeItemAtPath:self.localStoragePath error:nil]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback) callback(YES); + }); + + } else { + #ifdef DEBUG + NSLog(@"Error installing new app version %@", moveError); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback) callback(NO); + }); + } + } else { + #ifdef DEBUG + NSLog(@"Error removing current app version %@", deleteError); + #endif + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback) callback(NO); + }); + } + }); +} + +@end diff --git a/Updater/Updater/Updater.h b/Updater/Updater/Updater.h new file mode 100644 index 00000000..5c6e5f75 --- /dev/null +++ b/Updater/Updater/Updater.h @@ -0,0 +1,21 @@ +// +// Updater.h +// Updater +// +// Created by Guilherme Rambo on 01/05/15. +// Copyright (c) 2015 Guilherme Rambo. All rights reserved. +// + +#import + +//! Project version number for Updater. +FOUNDATION_EXPORT double UpdaterVersionNumber; + +//! Project version string for Updater. +FOUNDATION_EXPORT const unsigned char UpdaterVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import +#import \ No newline at end of file diff --git a/WWDC.xcodeproj/project.pbxproj b/WWDC.xcodeproj/project.pbxproj index c1a93a4e..afb9fe92 100644 --- a/WWDC.xcodeproj/project.pbxproj +++ b/WWDC.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ DD4F6E791AE2D98F0099BEB5 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F6E781AE2D98F0099BEB5 /* HeaderView.swift */; }; DD7F6F911AE97C99007A4E5E /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD7F6F901AE97C99007A4E5E /* WebKit.framework */; }; DD829F221AF271DF00B5977B /* index.json in Resources */ = {isa = PBXBuildFile; fileRef = DD829F211AF271DF00B5977B /* index.json */; }; + DD89EA0C1AF48A4C0073B845 /* Updater.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD89EA0B1AF48A300073B845 /* Updater.framework */; }; + DD89EA0D1AF48A4C0073B845 /* Updater.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DD89EA0B1AF48A300073B845 /* Updater.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DD92EAA61AE978EC00D503AE /* ASCIIwwdc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD92EAA41AE978DC00D503AE /* ASCIIwwdc.framework */; }; DD92EAA71AE978EC00D503AE /* ASCIIwwdc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DD92EAA41AE978DC00D503AE /* ASCIIwwdc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DD92EAAC1AE9795900D503AE /* TranscriptWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD92EAAA1AE9795900D503AE /* TranscriptWindowController.swift */; }; @@ -68,6 +70,20 @@ remoteGlobalIDString = DD2016401AE401B500DD8155; remoteInfo = ViewUtils; }; + DD89EA0A1AF48A300073B845 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DD89EA061AF48A300073B845 /* Updater.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DD89E9B91AF4255F0073B845; + remoteInfo = Updater; + }; + DD89EA0E1AF48A4C0073B845 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DD89EA061AF48A300073B845 /* Updater.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = DD89E9B81AF4255F0073B845; + remoteInfo = Updater; + }; DD92EAA31AE978DC00D503AE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DD92EA9F1AE978DC00D503AE /* ASCIIwwdc.xcodeproj */; @@ -93,6 +109,7 @@ files = ( DD2016691AE402B400DD8155 /* ViewUtils.framework in Embed Frameworks */, DD92EAA71AE978EC00D503AE /* ASCIIwwdc.framework in Embed Frameworks */, + DD89EA0D1AF48A4C0073B845 /* Updater.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -133,6 +150,7 @@ DD7F6F901AE97C99007A4E5E /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; DD829F1B1AF26D3C00B5977B /* Crashlytics.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Crashlytics.sh; sourceTree = ""; }; DD829F211AF271DF00B5977B /* index.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = index.json; sourceTree = ""; }; + DD89EA061AF48A300073B845 /* Updater.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Updater.xcodeproj; path = Updater/Updater.xcodeproj; sourceTree = ""; }; DD92EA9F1AE978DC00D503AE /* ASCIIwwdc.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ASCIIwwdc.xcodeproj; path = ASCIIwwdc/ASCIIwwdc.xcodeproj; sourceTree = ""; }; DD92EAAA1AE9795900D503AE /* TranscriptWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranscriptWindowController.swift; sourceTree = ""; }; DD92EAAB1AE9795900D503AE /* TranscriptWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TranscriptWindowController.xib; sourceTree = ""; }; @@ -156,6 +174,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DD89EA0C1AF48A4C0073B845 /* Updater.framework in Frameworks */, DD7F6F911AE97C99007A4E5E /* WebKit.framework in Frameworks */, DD2016681AE402B400DD8155 /* ViewUtils.framework in Frameworks */, DDCC5D721AF1518A00F24F55 /* Crashlytics.framework in Frameworks */, @@ -177,6 +196,7 @@ DD4F6E431AE2D11D0099BEB5 = { isa = PBXGroup; children = ( + DD89EA061AF48A300073B845 /* Updater.xcodeproj */, DDCC5D711AF1518A00F24F55 /* Crashlytics.framework */, DD7F6F901AE97C99007A4E5E /* WebKit.framework */, DD92EA9F1AE978DC00D503AE /* ASCIIwwdc.xcodeproj */, @@ -229,6 +249,14 @@ name = Service; sourceTree = ""; }; + DD89EA071AF48A300073B845 /* Products */ = { + isa = PBXGroup; + children = ( + DD89EA0B1AF48A300073B845 /* Updater.framework */, + ); + name = Products; + sourceTree = ""; + }; DD92EAA01AE978DC00D503AE /* Products */ = { isa = PBXGroup; children = ( @@ -351,6 +379,7 @@ dependencies = ( DD20166B1AE402B400DD8155 /* PBXTargetDependency */, DD92EAA91AE978EC00D503AE /* PBXTargetDependency */, + DD89EA0F1AF48A4C0073B845 /* PBXTargetDependency */, ); name = WWDC; productName = WWDC; @@ -388,6 +417,10 @@ ProductGroup = DD92EAA01AE978DC00D503AE /* Products */; ProjectRef = DD92EA9F1AE978DC00D503AE /* ASCIIwwdc.xcodeproj */; }, + { + ProductGroup = DD89EA071AF48A300073B845 /* Products */; + ProjectRef = DD89EA061AF48A300073B845 /* Updater.xcodeproj */; + }, { ProductGroup = DD2016621AE4029100DD8155 /* Products */; ProjectRef = DD2016611AE4029100DD8155 /* ViewUtils.xcodeproj */; @@ -408,6 +441,13 @@ remoteRef = DD2016651AE4029100DD8155 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + DD89EA0B1AF48A300073B845 /* Updater.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Updater.framework; + remoteRef = DD89EA0A1AF48A300073B845 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; DD92EAA41AE978DC00D503AE /* ASCIIwwdc.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; @@ -498,6 +538,11 @@ name = ViewUtils; targetProxy = DD20166A1AE402B400DD8155 /* PBXContainerItemProxy */; }; + DD89EA0F1AF48A4C0073B845 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Updater; + targetProxy = DD89EA0E1AF48A4C0073B845 /* PBXContainerItemProxy */; + }; DD92EAA91AE978EC00D503AE /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = ASCIIwwdc; diff --git a/WWDC/AppDelegate.swift b/WWDC/AppDelegate.swift index 0a429390..442c126d 100644 --- a/WWDC/AppDelegate.swift +++ b/WWDC/AppDelegate.swift @@ -8,6 +8,7 @@ import Cocoa import Crashlytics +import Updater @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @@ -20,6 +21,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidFinishLaunching(aNotification: NSNotification) { + // check for updates + checkForUpdates(nil) + // Keep a reference to the main application window window = NSApplication.sharedApplication().windows.last as! NSWindow? @@ -34,6 +38,33 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Insert code here to tear down your application } + @IBAction func checkForUpdates(sender: AnyObject?) { + UDUpdater.sharedUpdater().updateAutomatically = true + UDUpdater.sharedUpdater().checkForUpdatesWithCompletionHandler { newRelease in + if newRelease != nil { + if sender != nil { + let alert = NSAlert() + alert.messageText = "New version available" + alert.informativeText = "Version \(newRelease.version) is now available. It will be installed automatically the next time you launch the app." + alert.addButtonWithTitle("Ok") + alert.runModal() + } else { + let notification = NSUserNotification() + notification.title = "New version available" + notification.informativeText = "A new version is available, relaunch the app to update" + NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification(notification) + } + } else { + if sender != nil { + let alert = NSAlert() + alert.messageText = "You're up to date!" + alert.informativeText = "You have the newest version" + alert.addButtonWithTitle("Ok") + alert.runModal() + } + } + } + } } diff --git a/WWDC/Base.lproj/Main.storyboard b/WWDC/Base.lproj/Main.storyboard index 4c932e15..c94da1a9 100644 --- a/WWDC/Base.lproj/Main.storyboard +++ b/WWDC/Base.lproj/Main.storyboard @@ -21,6 +21,12 @@ + + + + + + diff --git a/WWDC/Info.plist b/WWDC/Info.plist index 5cdeec58..0a7c8c38 100644 --- a/WWDC/Info.plist +++ b/WWDC/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.6 + 1.6.1 CFBundleSignature ???? CFBundleVersion - 20 + 21 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright @@ -30,5 +30,9 @@ Main NSPrincipalClass NSApplication + UDRepoCreator + insidegui + UDRepoName + WWDC