mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-10 07:13:28 +01:00
Before, whether or not the source included the buildVersion affected the comparison. If present, the buildVersion was used in comparison, if not, only the version itself was used for comparsion. This meant it was impossible to update from a version with a buildVersion to the same version without one (e.g. going from betas to final releases). Now we _always_ consider the buildVersion in the comparsion, so an earlier entry in versions array without buildVersion can be considered “newer” even if versions match.
256 lines
9.5 KiB
Swift
256 lines
9.5 KiB
Swift
//
|
|
// InstallAppOperation.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 6/19/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Network
|
|
|
|
import AltStoreCore
|
|
import AltSign
|
|
import Roxas
|
|
|
|
@objc(InstallAppOperation)
|
|
class InstallAppOperation: ResultOperation<InstalledApp>
|
|
{
|
|
let context: InstallAppOperationContext
|
|
|
|
private var didCleanUp = false
|
|
|
|
init(context: InstallAppOperationContext)
|
|
{
|
|
self.context = context
|
|
|
|
super.init()
|
|
|
|
self.progress.totalUnitCount = 100
|
|
}
|
|
|
|
override func main()
|
|
{
|
|
super.main()
|
|
|
|
if let error = self.context.error
|
|
{
|
|
self.finish(.failure(error))
|
|
return
|
|
}
|
|
|
|
guard
|
|
let certificate = self.context.certificate,
|
|
let resignedApp = self.context.resignedApp,
|
|
let connection = self.context.installationConnection
|
|
else { return self.finish(.failure(OperationError.invalidParameters)) }
|
|
|
|
@Managed var appVersion = self.context.appVersion
|
|
let storeBuildVersion = $appVersion.buildVersion
|
|
|
|
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
|
backgroundContext.perform {
|
|
|
|
/* App */
|
|
let installedApp: InstalledApp
|
|
|
|
// Fetch + update rather than insert + resolve merge conflicts to prevent potential context-level conflicts.
|
|
if let app = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), self.context.bundleIdentifier), in: backgroundContext)
|
|
{
|
|
installedApp = app
|
|
}
|
|
else
|
|
{
|
|
installedApp = InstalledApp(resignedApp: resignedApp,
|
|
originalBundleIdentifier: self.context.bundleIdentifier,
|
|
certificateSerialNumber: certificate.serialNumber,
|
|
storeBuildVersion: storeBuildVersion,
|
|
context: backgroundContext)
|
|
}
|
|
|
|
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber, storeBuildVersion: storeBuildVersion)
|
|
installedApp.needsResign = false
|
|
|
|
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
|
|
{
|
|
installedApp.team = team
|
|
}
|
|
|
|
/* App Extensions */
|
|
var installedExtensions = Set<InstalledExtension>()
|
|
|
|
if
|
|
let bundle = Bundle(url: resignedApp.fileURL),
|
|
let directory = bundle.builtInPlugInsURL,
|
|
let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
|
|
{
|
|
for case let fileURL as URL in enumerator
|
|
{
|
|
guard let appExtensionBundle = Bundle(url: fileURL) else { continue }
|
|
guard let appExtension = ALTApplication(fileURL: appExtensionBundle.bundleURL) else { continue }
|
|
|
|
let parentBundleID = self.context.bundleIdentifier
|
|
let resignedParentBundleID = resignedApp.bundleIdentifier
|
|
|
|
let resignedBundleID = appExtension.bundleIdentifier
|
|
let originalBundleID = resignedBundleID.replacingOccurrences(of: resignedParentBundleID, with: parentBundleID)
|
|
|
|
let installedExtension: InstalledExtension
|
|
|
|
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
|
|
{
|
|
installedExtension = appExtension
|
|
}
|
|
else
|
|
{
|
|
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
|
|
}
|
|
|
|
installedExtension.update(resignedAppExtension: appExtension)
|
|
|
|
installedExtensions.insert(installedExtension)
|
|
}
|
|
}
|
|
|
|
installedApp.appExtensions = installedExtensions
|
|
|
|
self.context.beginInstallationHandler?(installedApp)
|
|
|
|
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
|
|
self.cleanUp()
|
|
|
|
var activeProfiles: Set<String>?
|
|
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
|
|
{
|
|
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
|
|
|
let fetchRequest = InstalledApp.activeAppsFetchRequest()
|
|
fetchRequest.includesPendingChanges = false
|
|
|
|
var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext)
|
|
if !activeApps.contains(installedApp)
|
|
{
|
|
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
|
|
|
|
let availableActiveApps = max(sideloadedAppsLimit - activeAppsCount, 0)
|
|
if installedApp.requiredActiveSlots <= availableActiveApps
|
|
{
|
|
// This app has not been explicitly activated, but there are enough slots available,
|
|
// so implicitly activate it.
|
|
installedApp.isActive = true
|
|
activeApps.append(installedApp)
|
|
}
|
|
else
|
|
{
|
|
installedApp.isActive = false
|
|
}
|
|
}
|
|
|
|
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
|
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
|
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
|
})
|
|
}
|
|
|
|
let request = BeginInstallationRequest(activeProfiles: activeProfiles, bundleIdentifier: installedApp.resignedBundleIdentifier)
|
|
connection.send(request) { (result) in
|
|
switch result
|
|
{
|
|
case .failure(let error): self.finish(.failure(error))
|
|
case .success:
|
|
|
|
self.receive(from: connection) { (result) in
|
|
switch result
|
|
{
|
|
case .success:
|
|
backgroundContext.perform {
|
|
installedApp.refreshedDate = Date()
|
|
self.finish(.success(installedApp))
|
|
}
|
|
|
|
case .failure(let error):
|
|
self.finish(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func finish(_ result: Result<InstalledApp, Error>)
|
|
{
|
|
self.cleanUp()
|
|
|
|
// Only remove refreshed IPA when finished.
|
|
if let app = self.context.app
|
|
{
|
|
let fileURL = InstalledApp.refreshedIPAURL(for: app)
|
|
|
|
do
|
|
{
|
|
try FileManager.default.removeItem(at: fileURL)
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to remove refreshed .ipa:", error)
|
|
}
|
|
}
|
|
|
|
super.finish(result)
|
|
}
|
|
}
|
|
|
|
private extension InstallAppOperation
|
|
{
|
|
func receive(from connection: ServerConnection, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
|
{
|
|
connection.receiveResponse() { (result) in
|
|
do
|
|
{
|
|
let response = try result.get()
|
|
print(response)
|
|
|
|
switch response
|
|
{
|
|
case .installationProgress(let response):
|
|
if response.progress == 1.0
|
|
{
|
|
self.progress.completedUnitCount = self.progress.totalUnitCount
|
|
completionHandler(.success(()))
|
|
}
|
|
else
|
|
{
|
|
self.progress.completedUnitCount = Int64(response.progress * 100)
|
|
self.receive(from: connection, completionHandler: completionHandler)
|
|
}
|
|
|
|
case .error(let response):
|
|
completionHandler(.failure(response.error))
|
|
|
|
default:
|
|
completionHandler(.failure(ALTServerError(.unknownRequest)))
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
completionHandler(.failure(ALTServerError(error)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func cleanUp()
|
|
{
|
|
guard !self.didCleanUp else { return }
|
|
self.didCleanUp = true
|
|
|
|
do
|
|
{
|
|
try FileManager.default.removeItem(at: self.context.temporaryDirectory)
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to remove temporary directory.", error)
|
|
}
|
|
}
|
|
}
|