//
// UKUpdateController.m
// UpdateKit
//
// Copyright © 2018 Graham Miln. All rights reserved. https://miln.eu
//
// This package is subject to the terms of the Artistic License 2.0.
// If a copy of the Artistic-2.0 was not distributed with this file, you can
// obtain one at https://indie.miln.eu/licence
@import UpdateCore;
#import "UKUpdateController.h"
// Class specific error codes
const NSInteger UKUpdateControllerErrorOpenPackage = 500;
// Info.plist keys
static NSString* UKUpdateControllerInfoPlistKeyBundleRevision = @"CFBundleVersion"; /**< Application revision */
static NSString* UKUpdateControllerInfoPlistKeyDiscoveryURL = @"UKDiscoveryURL"; /**< URL to fetch and parse for available updates. */
// KVO
static NSString* UKUpdateControllerKeyProgress = @"progress";
static NSString* UKUpdateControllerKeyProgressFractionCompleted = @"fractionCompleted";
static NSString* UKUpdateControllerKeyProgressLocalizedDescription = @"localizedDescription";
// ...contexts
static NSString* UKUpdateControllerKVOContextProgress = @"UKUpdateControllerKVOContextProgress";
static NSString* UKUpdateControllerKVOContextProgressState = @"UKUpdateControllerKVOContextProgressState";
static NSString* UKUpdateControllerKVOContextProgressLocalized = @"UKUpdateControllerKVOContextProgressLocalized";
/** UKUpdate combines an update with the discoverer's installer block. */
@interface UKUpdate : NSObject <UKAvailableUpdate>
@property(strong) UKUpdateController* updateController;
@property(strong) UCUpdate* update;
- (instancetype)initWithUpdate:(UCUpdate*)inUpdate andController:(UKUpdateController*)inUpdateController;
@end
@interface UKUpdateController ()
@property(strong,readwrite) NSProgress* progress;
@property(strong) NSWindowController* windowController;
@property(assign,readwrite) double uiProgressFractionCompleted;
@property(copy,readwrite) NSString* uiProgressDetail;
@property(copy,readwrite) NSString* uiProgressText;
- (BOOL)updateInProgress;
- (NSProgress*)installUpdate:(UCUpdate*)inUpdate manually:(BOOL)inManually withCompletionHandler:(void(^ __nullable)(NSError* __nullable))inHandler;
@end
@implementation UKUpdateController
- (instancetype)init {
if ((self = [super init])) {
// Clear previously cached downloads
[UCUpdateDownload clearCache];
// Observe progress to propogate values via main-thread to properties. Simplifies user interface binding.
[self addObserver:self forKeyPath:UKUpdateControllerKeyProgress options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgress)];
}
return self;
}
- (void)dealloc {
self.progress = nil; // stop observing progress
[self removeObserver:self forKeyPath:UKUpdateControllerKeyProgress context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgress)];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == (__bridge void * _Nullable)(UKUpdateControllerKVOContextProgress)) {
// Key paths affecting user interface properties
NSArray* progressKeyPaths = @[UKUpdateControllerKeyProgressFractionCompleted, UKUpdateControllerKeyProgressLocalizedDescription];
// Stop observing old progress
id oldProgress = change[NSKeyValueChangeOldKey];
if ([oldProgress isKindOfClass:NSProgress.class] == YES) {
for(NSString* kp in progressKeyPaths) {
[(NSProgress*)oldProgress removeObserver:self forKeyPath:kp context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressState)];
}
}
// Start observing new progress
id newProgress = change[NSKeyValueChangeNewKey];
if ([newProgress isKindOfClass:NSProgress.class] == YES) {
for(NSString* kp in progressKeyPaths) {
[(NSProgress*)newProgress addObserver:self forKeyPath:kp options:NSKeyValueObservingOptionInitial context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressState)];
}
}
} else if (context == (__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressState)) {
// Progress has changed, propogate to user interface properties via main thread
if ([object isKindOfClass:NSProgress.class] == YES) {
NSProgress* p = (NSProgress*)object;
dispatch_async(dispatch_get_main_queue(), ^{
self.uiProgressFractionCompleted = p.fractionCompleted;
self.uiProgressDetail = p.localizedAdditionalDescription;
self.uiProgressText = p.localizedDescription;
});
}
} else if (context == (__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressLocalized)) {
NSString* v = [object valueForKeyPath:keyPath];
dispatch_async(dispatch_get_main_queue(), ^{
self.uiProgressDetail = v;
});
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark -
- (BOOL)updateInProgress {
return (self.progress != nil && self.progress.isFinished == NO);
}
#pragma mark - Methods without user interface
- (NSProgress*)discoverWithCompletionHandler:(void(^)(NSObject<UKAvailableUpdate>* inUpdate,NSError* inDiscoveryError))inHandler {
// Get the discovery URL either a property or from the Info.plist.
if (self.discoveryURL == nil) {
self.discoveryURL = [NSURL URLWithString:[NSBundle.mainBundle objectForInfoDictionaryKey:UKUpdateControllerInfoPlistKeyDiscoveryURL]];
if (self.discoveryURL == nil) {
if (inHandler != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
inHandler(nil, [NSError errorWithDomain:NSStringFromClass(self.class) code:404 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Missing discovery URL. Set programmatically or Info.plist key: %@", UKUpdateControllerInfoPlistKeyDiscoveryURL]}]);
});
}
return nil;
}
}
// Get the application's revision from the Info.plist.
NSString* revision = [NSBundle.mainBundle objectForInfoDictionaryKey:UKUpdateControllerInfoPlistKeyBundleRevision];
if (revision == nil) {
if (inHandler != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
inHandler(nil, [NSError errorWithDomain:NSStringFromClass(self.class) code:404 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Missing current revision. Set Info.plist key: %@", UKUpdateControllerInfoPlistKeyBundleRevision]}]);
});
}
return nil;
}
// Get the operating system version. Used to ignore application updates requiring newer operating system versions.
NSString* systemVersion = UCUpdateDiscover.systemVersion;
NSProgress* progress = [NSProgress progressWithTotalUnitCount:1];
self.progress = progress;
[progress becomeCurrentWithPendingUnitCount:1];
(void) [UCUpdateDiscover discoverWithURL:self.discoveryURL revision:revision systemVersion:systemVersion queue:nil completionHandler:^(UCUpdate* inUpdate, NSError* inError) {
// Package up the core update (containing URL) and this instance (providing installation method)
UKUpdate* update = nil;
if (inUpdate != nil) {
update = [[UKUpdate alloc] initWithUpdate:inUpdate andController:self];
}
dispatch_async(dispatch_get_main_queue(), ^{
self.progress = nil;
inHandler(update, inError);
});
}];
[progress resignCurrent];
return progress;
}
- (NSProgress*)installUpdate:(UCUpdate*)inUpdate manually:(BOOL)inManually withCompletionHandler:(void(^ __nullable)(NSError* __nullable))inHandler {
NSProgress* progress = [NSProgress progressWithTotalUnitCount:2];
self.progress = progress;
// Ensure authority to install before downloading and wasting bandwidth
[UCUpdateInstall requestAuthorityWithQueue:nil completionHandler:^(NSError* inAuthorityError) {
[progress becomeCurrentWithPendingUnitCount:1];
if (inAuthorityError != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self.progress = nil;
if (inHandler != nil) {
inHandler(inAuthorityError);
}
});
return;
}
// Authority to install has been granted, download
__block UCUpdateDownload* downloader = nil;
downloader = [UCUpdateDownload downloadWithRequest:[NSURLRequest requestWithURL:inUpdate.downloadURL] queue:nil completionHandler:^(NSURL* inFileURL, NSString* inCommonName, NSError* inDownloadError) {
NSLog(@"[%@] Download finished: %@, %@, %@", NSStringFromClass(self.class), inFileURL, inCommonName, inDownloadError);
[downloader removeObserver:self forKeyPath:UCUpdateDownloadKeyLocalizedProgress context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressLocalized)];
if (inDownloadError == nil) {
[progress becomeCurrentWithPendingUnitCount:1];
progress.cancellable = NO;
progress.pausable = NO;
if (inManually == NO) {
__block UCUpdateInstall* installer = nil;
installer = [UCUpdateInstall installWithFileURL:downloader.fileURL queue:nil completionHandler:^(NSError* inError) {
NSLog(@"[%@] Install finished: %@", NSStringFromClass(self.class), inError);
[installer removeObserver:self forKeyPath:UCUpdateInstallKeyLocalizedProgress context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressLocalized)];
dispatch_async(dispatch_get_main_queue(), ^{
self.progress = nil;
if (inHandler != nil) {
inHandler(inError);
}
});
}];
[installer addObserver:self forKeyPath:UCUpdateInstallKeyLocalizedProgress options:NSKeyValueObservingOptionInitial context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressLocalized)];
[progress resignCurrent];
} else {
// Manual installation; open file package and dimiss update interface
BOOL validOpen = [NSWorkspace.sharedWorkspace openURL:downloader.fileURL];
NSError* openError = nil;
NSLog(@"[%@] Manual install: %@ (%@)", NSStringFromClass(self.class), downloader.fileURL, (validOpen?@"Opened":@"Failed"));
if (validOpen == NO) {
openError = [NSError errorWithDomain:NSStringFromClass(self.class) code:UKUpdateControllerErrorOpenPackage userInfo:nil];
}
dispatch_async(dispatch_get_main_queue(), ^{
self.progress = nil;
if (inHandler != nil) {
inHandler(openError);
}
});
}
} else {
dispatch_async(dispatch_get_main_queue(), ^{
self.progress = nil;
if (inHandler != nil) {
inHandler(inDownloadError);
}
});
}
}];
[downloader addObserver:self forKeyPath:UCUpdateDownloadKeyLocalizedProgress options:NSKeyValueObservingOptionInitial context:(__bridge void * _Nullable)(UKUpdateControllerKVOContextProgressLocalized)];
[progress resignCurrent];
}];
return progress;
}
@end
@implementation UKUpdate
- (instancetype)initWithUpdate:(UCUpdate*)inUpdate andController:(UKUpdateController*)inUpdateController {
NSParameterAssert(inUpdate != nil);
NSParameterAssert(inUpdateController != nil);
if ((self = [super init])) {
self.updateController = inUpdateController;
self.update = inUpdate;
}
return self;
}
- (NSProgress*)installWithCompletionHandler:(void(^ __nullable)(NSError* __nullable inInstallError))inHandler {
return [self.updateController installUpdate:self.update manually:NO withCompletionHandler:inHandler];
}
- (NSProgress*)installManuallyWithCompletionHandler:(void(^ __nullable)(NSError* __nullable inDownloadError))inHandler {
return [self.updateController installUpdate:self.update manually:YES withCompletionHandler:inHandler];
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@[%@]", NSStringFromClass(self.class), self.update.description];
}
@end
UKUpdateController.m
This file can be downloaded as part of milnupdate.tbz.