Skip to content

Commit

Permalink
Decouple and simplify installation code using protocols
Browse files Browse the repository at this point in the history
This also fixes a bug where the status window would stay visible during interactive (non-guided) package installations.
  • Loading branch information
zorgiepoo committed Dec 26, 2016
1 parent 21f3c8a commit 1ecb620
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 157 deletions.
2 changes: 2 additions & 0 deletions Sparkle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@
61F614540E24A12D009F47E7 /* it */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Sparkle.strings; sourceTree = "<group>"; };
61F83F6F0DBFE137006FDD30 /* SUBasicUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUBasicUpdateDriver.h; sourceTree = "<group>"; };
61F83F700DBFE137006FDD30 /* SUBasicUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUBasicUpdateDriver.m; sourceTree = "<group>"; };
7205C42E1E11A6DA00E370AE /* SUInstallerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUInstallerProtocol.h; sourceTree = "<group>"; };
720B16421C66433D006985FB /* UITests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "UITests-Info.plist"; path = "UITests/UITests-Info.plist"; sourceTree = SOURCE_ROOT; };
720B16431C66433D006985FB /* SUTestApplicationTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SUTestApplicationTest.swift; path = UITests/SUTestApplicationTest.swift; sourceTree = SOURCE_ROOT; };
7210C7671B9A9A1500EB90AC /* SUUnarchiverTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUUnarchiverTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1105,6 +1106,7 @@
7275F9C01B5F1F2900B1D19E /* SUFileManager.m */,
767B61AA1972D488004E0C3C /* SUGuidedPackageInstaller.h */,
767B61AB1972D488004E0C3C /* SUGuidedPackageInstaller.m */,
7205C42E1E11A6DA00E370AE /* SUInstallerProtocol.h */,
618FA4FF0DAE88B40026945C /* SUInstaller.h */,
618FA5000DAE88B40026945C /* SUInstaller.m */,
618FA5200DAE8E8A0026945C /* SUPackageInstaller.h */,
Expand Down
106 changes: 59 additions & 47 deletions Sparkle/Autoupdate/Autoupdate.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#import "SUStandardVersionComparator.h"
#import "SUStatusController.h"
#import "SULog.h"
#import "SUInstallerProtocol.h"

#include <unistd.h>

Expand Down Expand Up @@ -154,69 +155,80 @@ - (void)applicationDidFinishLaunching:(NSNotification __unused *)notification
}];
}

- (NSString *)installationPathForBundle:(NSBundle *)bundle
- (void)showError:(NSError *)error
{
if (SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME) {
// We'll install to "#{CFBundleName}.app", but only if that path doesn't already exist. If we're "Foo 4.2.app," and there's a "Foo.app" in this directory, we don't want to overwrite it! But if there's no "Foo.app," we'll take that name.
NSString *normalizedAppPath = [[[bundle bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", [bundle objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey], [[bundle bundlePath] pathExtension]]];

if (![[NSFileManager defaultManager] fileExistsAtPath:normalizedAppPath]) {
return normalizedAppPath;
}
if (self.shouldShowUI) {
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = @"";
alert.informativeText = [NSString stringWithFormat:@"%@", [error localizedDescription]];
[alert runModal];
}
return [bundle bundlePath];
}

- (void)install
{
NSBundle *theBundle = [NSBundle bundleWithPath:self.hostPath];
SUHost *host = [[SUHost alloc] initWithBundle:theBundle];
NSString *installationPath = [self installationPathForBundle:theBundle];

if (self.shouldShowUI) {
NSString *fileOperationToolPath = [[[[NSBundle mainBundle] executablePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@""SPARKLE_FILEOP_TOOL_NAME];

if (![[NSFileManager defaultManager] fileExistsAtPath:fileOperationToolPath]) {
SULog(@"Potential Installation Error: File operation tool path %@ is not found", fileOperationToolPath);
}

NSError *retrieveInstallerError = nil;
id<SUInstallerProtocol> installer = [SUInstaller installerForHost:host fileOperationToolPath:fileOperationToolPath updateDirectory:self.updateFolderPath error:&retrieveInstallerError];
if (installer == nil) {
SULog(@"Retrieved Installer Error: %@", retrieveInstallerError);
exit(EXIT_FAILURE);
}

if (self.shouldShowUI && [installer canInstallSilently]) {
self.statusController = [[SUStatusController alloc] initWithHost:host];
[self.statusController setButtonTitle:SULocalizedString(@"Cancel Update", @"") target:nil action:Nil isDefault:NO];
[self.statusController beginActionWithTitle:SULocalizedString(@"Installing update...", @"")
maxProgressValue: 0 statusText: @""];
[self.statusController showWindow:self];
}

NSString *fileOperationToolPath = [[[[NSBundle mainBundle] executablePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@""SPARKLE_FILEOP_TOOL_NAME];

if (![[NSFileManager defaultManager] fileExistsAtPath:fileOperationToolPath]) {
SULog(@"Potential Installation Error: File operation tool path %@ is not found", fileOperationToolPath);
}

[SUInstaller installFromUpdateFolder:self.updateFolderPath
overHost:host
installationPath:installationPath
fileOperationToolPath:fileOperationToolPath
versionComparator:[SUStandardVersionComparator defaultComparator]
completionHandler:^(NSError *error) {
if (error) {
NSError *underlyingError = [error.userInfo objectForKey:NSUnderlyingErrorKey];
if (underlyingError == nil || underlyingError.code != SUInstallationCancelledError) {
SULog(@"Installation Error: %@", error);
if (self.shouldShowUI) {
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = @"";
alert.informativeText = [NSString stringWithFormat:@"%@", [error localizedDescription]];
[alert runModal];
}
}
exit(EXIT_FAILURE);
} else {
NSString *pathToRelaunch = nil;
// If the installation path differs from the host path, we give higher precedence for it than
// if the desired relaunch path differs from the host path
if (![installationPath.pathComponents isEqualToArray:self.hostPath.pathComponents] || [self.relaunchPath.pathComponents isEqualToArray:self.hostPath.pathComponents]) {
pathToRelaunch = installationPath;
} else {
pathToRelaunch = self.relaunchPath;
}
[self cleanupAndTerminateWithPathToRelaunch:pathToRelaunch];
}
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *initialInstallationError = nil;
if (![installer performInitialInstallation:&initialInstallationError]) {
SULog(@"Failed to perform initial installation with error: %@", initialInstallationError);
dispatch_async(dispatch_get_main_queue(), ^{
[self showError:initialInstallationError];
exit(EXIT_FAILURE);
});
return;
}

NSError *finalInstallationError = nil;
if (![installer performFinalInstallation:&finalInstallationError]) {
NSError *underlyingError = [finalInstallationError.userInfo objectForKey:NSUnderlyingErrorKey];
dispatch_async(dispatch_get_main_queue(), ^{
if (underlyingError == nil || underlyingError.code != SUInstallationCancelledError) {
SULog(@"Failed to perform final installation Error: %@", finalInstallationError);
[self showError:finalInstallationError];
}
exit(EXIT_FAILURE);
});
return;
}

NSString *installationPath = [installer installationPath];

dispatch_async(dispatch_get_main_queue(), ^{
NSString *pathToRelaunch = nil;
// If the relaunch path is the same as the host bundle path, use the installation path from the installer which may be normalized
// Otherwise use the requested relaunch path in all other cases
if ([self.relaunchPath.pathComponents isEqualToArray:host.bundlePath.pathComponents]) {
pathToRelaunch = installationPath;
} else {
pathToRelaunch = self.relaunchPath;
}
[self cleanupAndTerminateWithPathToRelaunch:pathToRelaunch];
});
});
}

- (void)cleanupAndTerminateWithPathToRelaunch:(NSString *)relaunchPath
Expand Down
15 changes: 7 additions & 8 deletions Sparkle/SUGuidedPackageInstaller.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@ A guided installation can be started by applications other than the application
- Replace the use of `AuthorizationExecuteWithPrivilegesAndWait`. This method remains because it is well supported and tested. Ideally a helper tool or XPC would be used.
*/

#ifndef SUGUIDEDPACKAGEINSTALLER_H
#define SUGUIDEDPACKAGEINSTALLER_H

#import <Foundation/Foundation.h>
#import "SUInstaller.h"
#import "SUInstallerProtocol.h"

NS_ASSUME_NONNULL_BEGIN

@interface SUGuidedPackageInstaller : NSObject <SUInstallerProtocol>

@interface SUGuidedPackageInstaller : SUInstaller
- (instancetype)initWithPackagePath:(NSString *)packagePath installationPath:(NSString *)installationPath fileOperationToolPath:(NSString *)fileOperationToolPath;

/*! Perform the guided installation */
+ (void)performInstallationToPath:(NSString *)path fromPath:(NSString *)installerGuide host:(SUHost *)host fileOperationToolPath:(NSString *)fileOperationToolPath versionComparator:(id<SUVersionComparison>)comparator completionHandler:(void (^)(NSError *))completionHandler;
@end

#endif
NS_ASSUME_NONNULL_END
52 changes: 36 additions & 16 deletions Sparkle/SUGuidedPackageInstaller.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,46 @@
#import "SUGuidedPackageInstaller.h"
#import "SUFileManager.h"

@interface SUGuidedPackageInstaller ()

@property (nonatomic, readonly, copy) NSString *packagePath;
@property (nonatomic, readonly, copy) NSString *installationPath;
@property (nonatomic, readonly, copy) NSString *fileOperationToolPath;

@end

@implementation SUGuidedPackageInstaller

+ (void)performInstallationToPath:(NSString *)destinationPath fromPath:(NSString *)packagePath host:(SUHost *)__unused host fileOperationToolPath:(NSString *)fileOperationToolPath versionComparator:(id<SUVersionComparison>)__unused comparator completionHandler:(void (^)(NSError *))completionHandler
@synthesize packagePath = _packagePath;
@synthesize installationPath = _installationPath;
@synthesize fileOperationToolPath = _fileOperationToolPath;

- (instancetype)initWithPackagePath:(NSString *)packagePath installationPath:(NSString *)installationPath fileOperationToolPath:(NSString *)fileOperationToolPath
{
self = [super init];
if (self != nil) {
_packagePath = [packagePath copy];
_installationPath = [installationPath copy];
_fileOperationToolPath = [fileOperationToolPath copy];
}
return self;
}

- (BOOL)performInitialInstallation:(NSError * __autoreleasing *)__unused error
{
return YES;
}

- (BOOL)performFinalInstallation:(NSError * __autoreleasing *)error
{
NSParameterAssert(packagePath);
SUFileManager *fileManager = [SUFileManager fileManagerWithAuthorizationToolPath:self.fileOperationToolPath];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
SUFileManager *fileManager = [SUFileManager fileManagerWithAuthorizationToolPath:fileOperationToolPath];

NSError *error = nil;
BOOL validInstallation = [fileManager executePackageAtURL:[NSURL fileURLWithPath:packagePath] error:&error];

dispatch_async(dispatch_get_main_queue(), ^{
[self finishInstallationToPath:destinationPath
withResult:validInstallation
error:error
completionHandler:completionHandler];

});
});
return [fileManager executePackageAtURL:[NSURL fileURLWithPath:self.packagePath] error:error];
}

- (BOOL)canInstallSilently
{
return YES;
}

@end
16 changes: 9 additions & 7 deletions Sparkle/SUInstaller.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
// Copyright 2008 Andy Matuschak. All rights reserved.
//

#ifndef SUINSTALLER_H
#define SUINSTALLER_H

#import <Foundation/Foundation.h>
#import "SUVersionComparisonProtocol.h"

NS_ASSUME_NONNULL_BEGIN

@class SUHost;

@protocol SUInstallerProtocol;

@interface SUInstaller : NSObject

+ (NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host isPackage:(BOOL *)isPackagePtr isGuided:(BOOL *)isGuidedPtr;
+ (void)installFromUpdateFolder:(NSString *)inUpdateFolder overHost:(SUHost *)host installationPath:(NSString *)installationPath fileOperationToolPath:(NSString *)fileOperationToolPath versionComparator:(id<SUVersionComparison>)comparator completionHandler:(void (^)(NSError *))completionHandler;
+ (void)finishInstallationToPath:(NSString *)installationPath withResult:(BOOL)result error:(NSError *)error completionHandler:(void (^)(NSError *))completionHandler;
+ (nullable id<SUInstallerProtocol>)installerForHost:(SUHost *)host fileOperationToolPath:(NSString *)fileOperationToolPath updateDirectory:(NSString *)updateDirectory error:(NSError **)error;

+ (nullable NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host isPackage:(BOOL *)isPackagePtr isGuided:(nullable BOOL *)isGuidedPtr;

@end

#endif
NS_ASSUME_NONNULL_END
62 changes: 38 additions & 24 deletions Sparkle/SUInstaller.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import "SUInstaller.h"
#import "SUInstallerProtocol.h"
#import "SUPlainInstaller.h"
#import "SUPackageInstaller.h"
#import "SUGuidedPackageInstaller.h"
Expand All @@ -25,7 +26,7 @@ + (BOOL)isAliasFolderAtPath:(NSString *)path
return aliasFlag.boolValue && directoryFlag.boolValue;
}

+ (NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host isPackage:(BOOL *)isPackagePtr isGuided:(BOOL *)isGuidedPtr
+ (nullable NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host isPackage:(BOOL *)isPackagePtr isGuided:(nullable BOOL *)isGuidedPtr
{
NSParameterAssert(inUpdateFolder);
NSParameterAssert(host);
Expand Down Expand Up @@ -103,39 +104,52 @@ + (NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost
return newAppDownloadPath;
}

+ (void)installFromUpdateFolder:(NSString *)inUpdateFolder overHost:(SUHost *)host installationPath:(NSString *)installationPath fileOperationToolPath:(NSString *)fileOperationToolPath versionComparator:(id<SUVersionComparison>)comparator completionHandler:(void (^)(NSError *))completionHandler
+ (nullable id<SUInstallerProtocol>)installerForHost:(SUHost *)host fileOperationToolPath:(NSString *)fileOperationToolPath updateDirectory:(NSString *)updateDirectory error:(NSError * __autoreleasing *)error
{
BOOL isPackage = NO;
BOOL isGuided = NO;
NSString *newAppDownloadPath = [self installSourcePathInUpdateFolder:inUpdateFolder forHost:host isPackage:&isPackage isGuided:&isGuided];

if (newAppDownloadPath == nil) {
[self finishInstallationToPath:installationPath withResult:NO error:[NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't find an appropriate update in the downloaded package." }] completionHandler:completionHandler];
NSString *newDownloadPath = [self installSourcePathInUpdateFolder:updateDirectory forHost:host isPackage:&isPackage isGuided:&isGuided];

if (newDownloadPath == nil) {
if (error != NULL) {
*error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't find an appropriate update in the downloaded package." }];
}
return nil;
}

id <SUInstallerProtocol> installer;
if (isPackage && isGuided) {
installer = [[SUGuidedPackageInstaller alloc] initWithPackagePath:newDownloadPath installationPath:host.bundlePath fileOperationToolPath:fileOperationToolPath];
} else if (isPackage) {
installer = [[SUPackageInstaller alloc] initWithPackagePath:newDownloadPath installationPath:host.bundlePath];
} else {
if (isPackage && isGuided) {
[SUGuidedPackageInstaller performInstallationToPath:installationPath fromPath:newAppDownloadPath host:host fileOperationToolPath:fileOperationToolPath versionComparator:comparator completionHandler:completionHandler];
} else if (isPackage) {
[SUPackageInstaller performInstallationToPath:installationPath fromPath:newAppDownloadPath host:host fileOperationToolPath:fileOperationToolPath versionComparator:comparator completionHandler:completionHandler];
NSString *normalizedInstallationPath = nil;
if (SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME) {
normalizedInstallationPath = [self normalizedInstallationPathForHost:host];
}

// If we have a normalized path, we'll install to "#{CFBundleName}.app", but only if that path doesn't already exist. If we're "Foo 4.2.app," and there's a "Foo.app" in this directory, we don't want to overwrite it! But if there's no "Foo.app," we'll take that name.
// Otherwise if there's no normalized path (the more likely case), we'll just use the host bundle's path
NSString *installationPath;
if (normalizedInstallationPath != nil && ![[NSFileManager defaultManager] fileExistsAtPath:normalizedInstallationPath]) {
installationPath = normalizedInstallationPath;
} else {
[SUPlainInstaller performInstallationToPath:installationPath fromPath:newAppDownloadPath host:host fileOperationToolPath:fileOperationToolPath versionComparator:comparator completionHandler:completionHandler];
installationPath = host.bundlePath;
}

installer = [[SUPlainInstaller alloc] initWithHost:host bundlePath:newDownloadPath installationPath:installationPath fileOperationToolPath:fileOperationToolPath];
}
return installer;
}

+ (void)finishInstallationToPath:(NSString *)__unused installationPath withResult:(BOOL)result error:(NSError *)error completionHandler:(void (^)(NSError *))completionHandler
+ (NSString *)normalizedInstallationPathForHost:(SUHost *)host
{
if (result) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil);
});
} else {
if (!error) {
error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:nil];
}
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(error);
});
}
NSBundle *bundle = host.bundle;
assert(bundle != nil);

NSString *normalizedAppPath = [[[bundle bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", [bundle objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey], [[bundle bundlePath] pathExtension]]];

return normalizedAppPath;
}

@end
Loading

0 comments on commit 1ecb620

Please sign in to comment.