Files
SideStore/AltStore/Operations/InstallAppOperation.swift

262 lines
11 KiB
Swift
Raw Normal View History

//
// 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
import minimuxer
@objc(InstallAppOperation)
final 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,
Implement emotional damage (#95) * Implement em_proxy * Update libimobiledevice * Add minimuxer library to Xcode * Build missing C files for libimobiledevice * Remove objective C library * Add pairing file to Info.plist * Heartbeat self on startup * Enable JIT on-device * Implement on-device installation * Fix OpenSSL header errors * Random submodule bullcrap go * Search release folder for emotional damage * Clean dependencies * Build Rust dependencies attempt 1/999 * Update em_proxy * Implement refreshing apps * Clean up old operations * Remove all AltServer code * Remove files from Xcode project * Implement auto mounting the developer DMG * Recover from app being backgrounded * Fixed keeping pairing file in app after updating SideStore (#3) * Use compliant error handling for minimuxer * Fix app failing to install * Don't kill proxy on backgrounding * Makes sure the ALTPairingFile gets transferred even if team IDs change (#4) * Step 1 to allow SideStore to resign itself * Update ResignAppOperation.swift * Adding cache for action runner (#5) * Start caching commit for actions Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Update build.yml Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Update build.yml Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Use rust lib directories to cache Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Cache cargo also Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Fix spacing Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Replace cargo id for caching Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Remove cache if statements Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> * Add disconnected WireGuard detection * Add minimuxer logging Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com> Co-authored-by: jawshoeadan <62785552+jawshoeadan@users.noreply.github.com> Co-authored-by: Joelle Stickney <joellestickney@gmail.com> Co-authored-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2022-11-02 17:58:59 -07:00
let resignedApp = self.context.resignedApp
else { return self.finish(.failure(OperationError.invalidParameters)) }
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, context: backgroundContext)
}
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber)
installedApp.needsResign = false
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
{
installedApp.team = team
}
/* App Extensions */
2020-01-21 16:53:34 -08:00
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)
print("`parentBundleID`: \(parentBundleID)")
print("`resignedParentBundleID`: \(resignedParentBundleID)")
print("`resignedBundleID`: \(resignedBundleID)")
print("`originalBundleID`: \(originalBundleID)")
2020-01-21 16:53:34 -08:00
let installedExtension: InstalledExtension
if let appExtension = installedApp.appExtensions.first(where: { $0.bundleIdentifier == originalBundleID })
2020-01-21 16:53:34 -08:00
{
installedExtension = appExtension
}
else
{
installedExtension = InstalledExtension(resignedAppExtension: appExtension, originalBundleIdentifier: originalBundleID, context: backgroundContext)
2020-01-21 16:53:34 -08:00
}
installedExtension.update(resignedAppExtension: appExtension)
2020-01-21 16:53:34 -08:00
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
})
}
var installing = true
if installedApp.storeApp?.bundleIdentifier.range(of: Bundle.Info.appbundleIdentifier) != nil {
// Reinstalling ourself will hang until we leave the app, so we need to exit it without force closing
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if UIApplication.shared.applicationState != .active {
print("We are not in the foreground, let's not do anything")
return
}
if !installing {
print("Installing finished")
return
}
print("We are still installing after 3 seconds")
2023-07-27 05:10:27 -04:00
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch (settings.authorizationStatus) {
case .authorized, .ephemeral, .provisional:
print("Notifications are enabled")
let content = UNMutableNotificationContent()
content.title = "Refreshing..."
content.body = "SideStore will automatically move to the homescreen to finish refreshing!"
2023-07-27 05:10:27 -04:00
let notification = UNNotificationRequest(identifier: Bundle.Info.appbundleIdentifier + ".FinishRefreshNotification", content: content, trigger: UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false))
UNUserNotificationCenter.current().add(notification)
break
default:
print("Notifications are not enabled")
let alert = UIAlertController(title: "Finish Refresh", message: "Please reopen SideStore after the process is finished.To finish refreshing, SideStore must be moved to the background. To do this, you can either go to the Home Screen manually or by hitting Continue. Please reopen SideStore after doing this.", preferredStyle: .alert)
2023-07-27 05:10:27 -04:00
alert.addAction(UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default, handler: { _ in
print("Going home")
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}))
DispatchQueue.main.async {
let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(alert, animated: true)
} else {
print("No key window? Let's just go home")
}
}
}
}
2023-07-27 06:06:09 -04:00
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
}
var attempts = 10
while (attempts != 0){
print("Install ipa attempts left: \(attempts)")
do {
try install_ipa(installedApp.bundleIdentifier)
installing = false
installedApp.refreshedDate = Date()
self.finish(.success(installedApp))
break
} catch {
attempts -= 1
if (attempts <= 0){
installing = false
self.finish(.failure(MinimuxerError.InstallApp))
}
}
}
}
}
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)
print("Removed refreshed IPA")
}
catch
{
print("Failed to remove refreshed .ipa: \(error)")
}
}
super.finish(result)
}
}
private extension InstallAppOperation
{
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)
}
}
}