Files
Sparkle/Autoupdate/AppInstaller.m
Zorg 8125490931 Pre-warm installs before relaunch (#2421)
Move a lot of the installation work to be done during the first phase of installation (when possible), before the target app needs to be terminated. For the common case where the old and new bundles are on the same volume, the final phase of installation requiring a restart is now just an atomic swap.

Also move the termination listener to the agent app which resolves some launch/CI failures we've been seeing.
2023-08-27 22:22:52 -07:00

676 lines
33 KiB
Objective-C

//
// AppInstaller.m
// Sparkle
//
// Created by Mayur Pawashe on 3/7/16.
// Copyright © 2016 Sparkle Project. All rights reserved.
//
#import "AppInstaller.h"
#import "SUInstaller.h"
#import "SUUpdateValidator.h"
#import "SULog.h"
#import "SULog+NSError.h"
#import "SUHost.h"
#import "SULocalizations.h"
#import "SUStandardVersionComparator.h"
#import "SPUMessageTypes.h"
#import "SPUSecureCoding.h"
#import "SPUInstallationInputData.h"
#import "SUUnarchiver.h"
#import "SUFileManager.h"
#import "SPUInstallationInfo.h"
#import "SUAppcastItem.h"
#import "SUErrors.h"
#import "SUInstallerCommunicationProtocol.h"
#import "AgentConnection.h"
#import "SPUInstallerAgentProtocol.h"
#import "SPUInstallationType.h"
#import "SPULocalCacheDirectory.h"
#include "AppKitPrevention.h"
#define FIRST_UPDATER_MESSAGE_TIMEOUT 18ull
#define RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT 8ull
/**
* Show display progress UI after a delay from starting the final part of the installation.
* This should be long enough so that we don't show progress for very fast installations, but
* short enough so that we don't leave the user wondering why nothing is happening.
*/
static const NSTimeInterval SUDisplayProgressTimeDelay = 0.7;
@interface AppInstaller () <NSXPCListenerDelegate, SUInstallerCommunicationProtocol, AgentConnectionDelegate>
@end
@implementation AppInstaller
{
NSXPCListener* _xpcListener;
NSXPCConnection *_activeConnection;
id<SUInstallerCommunicationProtocol> _communicator;
AgentConnection *_agentConnection;
SUUpdateValidator *_updateValidator;
NSString *_hostBundleIdentifier;
NSString *_homeDirectory;
NSString *_userName;
SUHost *_host;
NSString *_updateDirectoryPath;
NSString *_downloadName;
NSString *_decryptionPassword;
SUSignatures *_signatures;
NSString *_relaunchPath;
NSString *_installationType;
id<SUInstallerProtocol> _installer;
dispatch_queue_t _installerQueue;
BOOL _shouldRelaunch;
BOOL _shouldShowUI;
BOOL _receivedUpdaterPong;
BOOL _willCompleteInstallation;
BOOL _receivedInstallationData;
BOOL _finishedValidation;
BOOL _agentInitiatedConnection;
BOOL _performedStage1Installation;
BOOL _performedStage2Installation;
BOOL _performedStage3Installation;
BOOL _targetTerminated;
}
- (instancetype)initWithHostBundleIdentifier:(NSString *)hostBundleIdentifier homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName
{
if (!(self = [super init])) {
return nil;
}
_hostBundleIdentifier = [hostBundleIdentifier copy];
_homeDirectory = [homeDirectory copy];
_userName = [userName copy];
_xpcListener = [[NSXPCListener alloc] initWithMachServiceName:SPUInstallerServiceNameForBundleIdentifier(hostBundleIdentifier)];
_xpcListener.delegate = self;
_agentConnection = [[AgentConnection alloc] initWithHostBundleIdentifier:hostBundleIdentifier delegate:self];
return self;
}
- (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection
{
if (_activeConnection != nil) {
SULog(SULogLevelDefault, @"Rejecting multiple connections...");
[newConnection invalidate];
return NO;
}
_activeConnection = newConnection;
newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)];
newConnection.exportedObject = self;
newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)];
__weak __typeof__(self) weakSelf = self;
newConnection.interruptionHandler = ^{
dispatch_async(dispatch_get_main_queue(), ^{
__typeof__(self) strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf->_activeConnection invalidate];
}
});
};
newConnection.invalidationHandler = ^{
dispatch_async(dispatch_get_main_queue(), ^{
__typeof__(self) strongSelf = weakSelf;
if (strongSelf != nil) {
if (strongSelf->_activeConnection != nil && !strongSelf->_willCompleteInstallation) {
[strongSelf cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Invalidation on remote port being called, and installation is not close enough to completion!" }]];
}
strongSelf->_communicator = nil;
strongSelf->_activeConnection = nil;
}
});
};
[newConnection resume];
_communicator = newConnection.remoteObjectProxy;
return YES;
}
- (void)start
{
[_xpcListener resume];
[_agentConnection startListener];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(FIRST_UPDATER_MESSAGE_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!self->_receivedInstallationData) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: installation data was never received" }]];
}
if (!self->_agentConnection.connected) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: agent connection was never initiated" }]];
}
});
}
- (void)extractAndInstallUpdate SPU_OBJC_DIRECT
{
[_communicator handleMessageWithIdentifier:SPUExtractionStarted data:[NSData data]];
NSString *archivePath = [_updateDirectoryPath stringByAppendingPathComponent:_downloadName];
id<SUUnarchiverProtocol> unarchiver = [SUUnarchiver unarchiverForPath:archivePath updatingHostBundlePath:_host.bundlePath decryptionPassword:_decryptionPassword expectingInstallationType:_installationType];
NSError *unarchiverError = nil;
BOOL success = NO;
if (!unarchiver) {
unarchiverError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No valid unarchiver was found for %@", archivePath] }];
success = NO;
} else {
_updateValidator = [[SUUpdateValidator alloc] initWithDownloadPath:archivePath signatures:_signatures host:_host];
// Delta & package updates will require validation before extraction
// Normal application updates are a bit more lenient allowing developers to change one of apple dev ID or EdDSA keys
BOOL needsPrevalidation = [[unarchiver class] mustValidateBeforeExtraction] || ![_installationType isEqualToString:SPUInstallationTypeApplication];
if (needsPrevalidation) {
success = [_updateValidator validateDownloadPathWithError:&unarchiverError];
} else {
success = YES;
}
}
if (!success) {
[self unarchiverDidFailWithError:unarchiverError];
} else {
[unarchiver
unarchiveWithCompletionBlock:^(NSError * _Nullable error) {
if (error != nil) {
[self unarchiverDidFailWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to unarchive %@", archivePath], NSUnderlyingErrorKey: (NSError * _Nonnull)error }]];
} else {
[self->_communicator handleMessageWithIdentifier:SPUValidationStarted data:[NSData data]];
NSError *validationError = nil;
BOOL validationSuccess = [self->_updateValidator validateWithUpdateDirectory:self->_updateDirectoryPath error:&validationError];
if (!validationSuccess) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Update validation was a failure", NSUnderlyingErrorKey: validationError }]];
} else {
[self->_communicator handleMessageWithIdentifier:SPUInstallationStartedStage1 data:[NSData data]];
self->_finishedValidation = YES;
if (self->_agentInitiatedConnection) {
[self retrieveProcessIdentifierAndStartInstallation];
}
}
}
}
progressBlock:^(double progress) {
if (sizeof(progress) == sizeof(uint64_t)) {
uint64_t progressValue = CFSwapInt64HostToLittle(*(uint64_t *)&progress);
NSData *data = [NSData dataWithBytes:&progressValue length:sizeof(progressValue)];
[self->_communicator handleMessageWithIdentifier:SPUExtractedArchiveWithProgress data:data];
}
}];
}
}
- (void)clearUpdateDirectory SPU_OBJC_DIRECT
{
if (_updateDirectoryPath != nil) {
NSError *theError = nil;
if (![[[SUFileManager alloc] init] removeItemAtURL:[NSURL fileURLWithPath:_updateDirectoryPath] error:&theError]) {
SULog(SULogLevelError, @"Couldn't remove update folder: %@.", theError);
}
_updateDirectoryPath = nil;
}
}
- (void)unarchiverDidFailWithError:(NSError *)error SPU_OBJC_DIRECT
{
SULog(SULogLevelError, @"Failed to unarchive file");
SULogError(error);
// No longer need update validator until next possible extraction (eg: if initial delta update fails)
_updateValidator = nil;
// Client could try update again with different inputs
// Eg: one common case is if a delta update fails, client may want to fall back to regular update
// We really only need to set updateDirectoryPath to nil since that's the field we check if we've received installation data,
// but may as well set other fields to nil too
[self clearUpdateDirectory];
_downloadName = nil;
_decryptionPassword = nil;
_signatures = nil;
_relaunchPath = nil;
_host = nil;
NSData *archivedError = SPUArchiveRootObjectSecurely(error);
[_communicator handleMessageWithIdentifier:SPUArchiveExtractionFailed data:archivedError != nil ? archivedError : [NSData data]];
}
- (void)agentConnectionDidInitiate
{
_agentInitiatedConnection = YES;
if (_finishedValidation) {
[self retrieveProcessIdentifierAndStartInstallation];
}
}
- (void)agentConnectionDidInvalidate
{
if (!_finishedValidation || !_agentInitiatedConnection || !_targetTerminated) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: @"Error: Agent connection invalidated before installation began" }];
NSError *agentError = _agentConnection.invalidationError;
if (agentError != nil) {
userInfo[NSUnderlyingErrorKey] = agentError;
}
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:userInfo]];
}
}
- (void)retrieveProcessIdentifierAndStartInstallation SPU_OBJC_DIRECT
{
// We use the relaunch path for the bundle to listen for termination instead of the host path
// For a plug-in this makes a big difference; we want to wait until the app hosting the plug-in terminates
// Otherwise for an app, the relaunch path and host path should be identical
__block BOOL receivedResponse = NO;
[_agentConnection.agent registerApplicationBundlePath:_relaunchPath reply:^(BOOL targetTerminated) {
dispatch_async(dispatch_get_main_queue(), ^{
receivedResponse = YES;
if (!targetTerminated) {
[self->_agentConnection.agent listenForTerminationWithCompletion:^{
dispatch_async(dispatch_get_main_queue(), ^{
self->_targetTerminated = YES;
if (self->_performedStage1Installation) {
[self finishInstallationAfterHostTermination];
}
});
}];
} else {
self->_targetTerminated = YES;
}
[self startInstallation];
});
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!receivedResponse) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout error: failed to retreive process identifier from agent" }]];
}
});
}
- (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data
{
if (identifier == SPUInstallationData && _updateDirectoryPath == nil) {
dispatch_async(dispatch_get_main_queue(), ^{
// Mark that we have received the installation data
// Do not rely on eg: self->_updateDirectoryPath != nil because we may set it to nil again if an early stage fails (i.e, archive extraction)
self->_receivedInstallationData = YES;
SPUInstallationInputData *installationData = (SPUInstallationInputData *)SPUUnarchiveRootObjectSecurely(data, [SPUInstallationInputData class]);
if (installationData == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to unarchive input installation data" }]];
return;
}
NSString *installationType = installationData.installationType;
if (!SPUValidInstallationType(installationType)) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received invalid installation type: %@", installationType] }]];
return;
}
NSBundle *hostBundle = [NSBundle bundleWithPath:installationData.hostBundlePath];
NSString *bundleIdentifier = hostBundle.bundleIdentifier;
if (bundleIdentifier == nil || ![bundleIdentifier isEqualToString:self->_hostBundleIdentifier]) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to match host bundle identifiers %@ and %@", self->_hostBundleIdentifier, bundleIdentifier] }]];
return;
}
// This will be important later
if (installationData.relaunchPath == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to obtain relaunch path from installation data" }]];
return;
}
// This installation path is specific to sparkle and the bundle identifier
NSString *rootCacheInstallationPath = [[SPULocalCacheDirectory cachePathForBundleIdentifier:bundleIdentifier] stringByAppendingPathComponent:@"Installation"];
[SPULocalCacheDirectory removeOldItemsInDirectory:rootCacheInstallationPath];
NSString *cacheInstallationPath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootCacheInstallationPath];
if (cacheInstallationPath == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation cache directory in %@", rootCacheInstallationPath] }]];
return;
}
// Resolve the bookmark data for the downloaded update
// See "Share file access between processes with URL bookmarks" in https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox
BOOL isStale = NO;
NSError *bookmarkError = nil;
NSURL *downloadURL = [NSURL URLByResolvingBookmarkData:installationData.updateURLBookmarkData options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&isStale error:&bookmarkError];
if (downloadURL == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resolve bookmark data from downloaded update", NSUnderlyingErrorKey: bookmarkError }]];
return;
}
// Validate the download URL before moving it
{
NSArray<NSString *> *downloadURLPathComponents = downloadURL.URLByResolvingSymlinksInPath.pathComponents;
if (downloadURLPathComponents == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve path components from download URL" }]];
return;
}
if ([downloadURLPathComponents containsObject:@".."]) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components contains '..' which is unsafe" }]];
return;
}
if (![downloadURLPathComponents containsObject:@SPARKLE_BUNDLE_IDENTIFIER] || ![downloadURLPathComponents containsObject:@"PersistentDownloads"]) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components does not contain PersistentDownloads or "@SPARKLE_BUNDLE_IDENTIFIER }]];
return;
}
}
if (!isStale) {
SULog(SULogLevelError, @"Error: bookmark data for update download is stale.. but still continuing.");
}
NSString *downloadName = downloadURL.lastPathComponent;
if (downloadName == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve download name from download URL" }]];
return;
}
// Move the download archive to somewhere where probably only we will be touching it
// This prevents eg: if a bug exists in the updater that removes files we are trying to install
// When this tool is ran as root, we are moving it into a directory that only root will have access to
NSURL *downloadDestinationURL = [[NSURL fileURLWithPath:cacheInstallationPath] URLByAppendingPathComponent:downloadName];
NSError *moveError = nil;
if (![[[SUFileManager alloc] init] moveItemAtURL:downloadURL toURL:downloadDestinationURL error:&moveError]) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to move download archive to new location", NSUnderlyingErrorKey: moveError }]];
return;
}
// Make sure the downloaded archive we moved over is a regular file and not a symbolic link placed by an attacker
NSError *attributesError = nil;
NSString *downloadDestinationPath = downloadDestinationURL.path;
if (downloadDestinationPath == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive path from %@", downloadDestinationURL] }]];
return;
}
NSDictionary<NSString *, id> *archiveAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:downloadDestinationPath error:&attributesError];
if (archiveAttributes == nil) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive attributes from %@", downloadDestinationPath] }]];
return;
}
if (![(NSString *)archiveAttributes[NSFileType] isEqualToString:NSFileTypeRegular]) {
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received bad archive file type: %@", archiveAttributes[NSFileType]] }]];
return;
}
// Carry these properities separately rather than using the SUInstallationInputData object
// Some of our properties may slightly differ than our input and we don't want to make the mistake of using one of those
self->_installationType = installationType;
self->_relaunchPath = installationData.relaunchPath;
self->_downloadName = downloadName;
self->_signatures = installationData.signatures;
self->_updateDirectoryPath = cacheInstallationPath;
self->_host = [[SUHost alloc] initWithBundle:hostBundle];
[self extractAndInstallUpdate];
});
} else if (identifier == SPUSentUpdateAppcastItemData) {
SUAppcastItem *updateItem = (SUAppcastItem *)SPUUnarchiveRootObjectSecurely(data, [SUAppcastItem class]);
if (updateItem != nil) {
SPUInstallationInfo *installationInfo = [[SPUInstallationInfo alloc] initWithAppcastItem:updateItem canSilentlyInstall:[_installer canInstallSilently]];
NSData *archivedData = SPUArchiveRootObjectSecurely(installationInfo);
if (archivedData != nil) {
[_agentConnection.agent registerInstallationInfoData:archivedData];
}
}
} else if (identifier == SPUResumeInstallationToStage2 && data.length == sizeof(uint8_t) * 2) {
// Because anyone can ask us to resume the installation, it may be wise to think about backwards compatibility here if IPC changes
uint8_t relaunch = *((const uint8_t *)data.bytes);
uint8_t showsUI = *((const uint8_t *)data.bytes + 1);
dispatch_async(dispatch_get_main_queue(), ^{
// This flag has an impact on interactive type installations and showing UI progress during non-interactive installations
self->_shouldShowUI = (BOOL)showsUI;
// Don't test if the application was alive initially, leave that to the progress agent if we decide to relaunch
self->_shouldRelaunch = (BOOL)relaunch;
if (self->_performedStage1Installation) {
// Resume the installation if we aren't done with stage 2 yet, and remind the client we are prepared to relaunch
dispatch_async(self->_installerQueue, ^{
if (!self->_performedStage2Installation) {
[self performStage2Installation];
} else if (!self->_performedStage3Installation) {
// If we already performed the 2nd stage, re-purpose this request to re-try sending another termination signal
dispatch_async(dispatch_get_main_queue(), ^{
// Don't check if the target is already terminated, leave that to the progress agent
// We could be slightly off if there were multiple instances running
[self->_agentConnection.agent sendTerminationSignal];
});
}
});
}
});
} else if (identifier == SPUCancelInstallation) {
dispatch_async(dispatch_get_main_queue(), ^{
[self cleanupAndExitWithStatus:0 error:nil];
});
} else if (identifier == SPUUpdaterAlivePong) {
_receivedUpdaterPong = YES;
}
}
- (void)startInstallation SPU_OBJC_DIRECT
{
_willCompleteInstallation = YES;
dispatch_queue_attr_t queuePriority = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
_installerQueue = dispatch_queue_create("org.sparkle-project.sparkle.installer", queuePriority);
dispatch_async(_installerQueue, ^{
NSError *installerError = nil;
id <SUInstallerProtocol> installer = [SUInstaller installerForHost:self->_host expectedInstallationType:self->_installationType updateDirectory:self->_updateDirectoryPath homeDirectory:self->_homeDirectory userName:self->_userName error:&installerError];
if (installer == nil) {
dispatch_async(dispatch_get_main_queue(), ^{
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to create installer instance", NSUnderlyingErrorKey: installerError }]];
});
return;
}
NSError *firstStageError = nil;
if (![installer performInitialInstallation:&firstStageError]) {
self->_installer = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to start installer", NSUnderlyingErrorKey: firstStageError }]];
});
return;
}
uint8_t canPerformSilentInstall = (uint8_t)[installer canInstallSilently];
dispatch_async(dispatch_get_main_queue(), ^{
self->_installer = installer;
uint8_t sendInformation[] = {canPerformSilentInstall, (uint8_t)self->_targetTerminated};
NSData *sendData = [NSData dataWithBytes:sendInformation length:sizeof(sendInformation)];
[self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage1 data:sendData];
self->_performedStage1Installation = YES;
if (self->_targetTerminated) {
// Stage 2 can still be run before we finish installation
// if the updater requests for it before the app is terminated
[self finishInstallationAfterHostTermination];
}
});
});
}
- (void)performStage2Installation SPU_OBJC_DIRECT
{
BOOL canPerformSecondStage = _shouldShowUI || [_installer canInstallSilently];
if (canPerformSecondStage) {
_performedStage2Installation = YES;
dispatch_async(dispatch_get_main_queue(), ^{
uint8_t targetTerminated = (uint8_t)self->_targetTerminated;
NSData *sendData = [NSData dataWithBytes:&targetTerminated length:sizeof(targetTerminated)];
[self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage2 data:sendData];
// Don't check if the target is already terminated, leave that to the progress agent
// We could be slightly off if there were multiple instances running
[self->_agentConnection.agent sendTerminationSignal];
});
} else {
_installer = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resume installer on stage 2 because installation cannot be installed silently" }]];
});
}
}
- (void)finishInstallationAfterHostTermination SPU_OBJC_DIRECT
{
assert(self->_targetTerminated);
// Show our installer progress UI tool if only after a certain amount of time passes,
// and if our installer is silent (i.e, doesn't show progress on its own)
__block BOOL shouldShowUIProgress = YES;
if (self->_shouldShowUI && [self->_installer canInstallSilently]) {
// Ask the updater if it is still alive
// If they are, we will receive a pong response back
// Reset if we received a pong just to be on the safe side
self->_receivedUpdaterPong = NO;
[self->_communicator handleMessageWithIdentifier:SPUUpdaterAlivePing data:[NSData data]];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SUDisplayProgressTimeDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// Make sure we're still eligible for showing the installer progress
// Also if the updater process is still alive, showing the progress should not be our duty
// if the communicator object is nil, the updater definitely isn't alive. However, if it is not nil,
// this does not necessarily mean the updater is alive, so we should also check if we got a recent response back from the updater
if (shouldShowUIProgress && (!self->_receivedUpdaterPong || self->_communicator == nil)) {
[self->_agentConnection.agent showProgress];
}
});
}
dispatch_async(self->_installerQueue, ^{
if (!self->_performedStage2Installation) {
[self performStage2Installation];
}
if (!self->_performedStage2Installation) {
// We failed and we're going to exit shortly
return;
}
NSError *thirdStageError = nil;
if (![self->_installer performFinalInstallationProgressBlock:nil error:&thirdStageError]) {
[self->_installer performCleanup];
self->_installer = nil;
dispatch_async(dispatch_get_main_queue(), ^{
[self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to finalize installation", NSUnderlyingErrorKey: thirdStageError }]];
});
return;
}
self->_performedStage3Installation = YES;
dispatch_async(dispatch_get_main_queue(), ^{
// Make sure to stop our displayed progress before we move onto cleanup & relaunch
// This will also stop the agent from broadcasting the status info service, which we want to do before
// we relaunch the app because the relaunched app could check the service upon launch..
[self->_agentConnection.agent stopProgress];
shouldShowUIProgress = NO;
[self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage3 data:[NSData data]];
if (self->_shouldRelaunch) {
// This will also signal to the agent that it will terminate soon
[self->_agentConnection.agent relaunchApplication];
}
[self->_installer performCleanup];
[self cleanupAndExitWithStatus:EXIT_SUCCESS error:nil];
});
});
}
- (void)cleanupAndExitWithStatus:(int)status error:(NSError * _Nullable)error __attribute__((noreturn))
{
if (error != nil) {
SULogError(error);
NSData *errorData = SPUArchiveRootObjectSecurely((NSError * _Nonnull)error);
if (errorData != nil) {
[_communicator handleMessageWithIdentifier:SPUInstallerError data:errorData];
}
}
// It's nice to tell the other end we're invalidating
[_activeConnection invalidate];
_activeConnection = nil;
[_xpcListener invalidate];
_xpcListener = nil;
[_agentConnection invalidate];
_agentConnection = nil;
[self clearUpdateDirectory];
exit(status);
}
@end