Files
SideStore/Sources/SideStoreAppKit/Operations/BackgroundRefreshAppsOperation.swift

229 lines
9.7 KiB
Swift
Raw Normal View History

//
// BackgroundRefreshAppsOperation.swift
// AltStore
//
// Created by Riley Testut on 7/6/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import CoreData
2023-03-01 00:48:36 -05:00
import UIKit
2023-03-01 00:48:36 -05:00
import SideStoreCore
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
import EmotionalDamage
2023-03-01 00:48:36 -05:00
enum RefreshError: LocalizedError {
case noInstalledApps
2023-03-01 00:48:36 -05:00
var errorDescription: String? {
2023-03-01 00:48:36 -05:00
switch self {
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
}
}
}
2023-03-01 00:48:36 -05:00
private extension CFNotificationName {
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
2023-03-01 00:48:36 -05:00
static func requestAppState(for appID: String) -> CFNotificationName {
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
2023-03-01 00:48:36 -05:00
static func appIsRunning(for appID: String) -> CFNotificationName {
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
}
2023-03-01 00:48:36 -05:00
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { _, observer, name, _, _ in
guard let name = name, let observer = observer else { return }
2023-03-01 00:48:36 -05:00
let operation = unsafeBitCast(observer, to: BackgroundRefreshAppsOperation.self)
operation.receivedApplicationState(notification: name)
}
@objc(BackgroundRefreshAppsOperation)
2023-03-01 19:09:33 -05:00
public final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]> {
public let installedApps: [InstalledApp]
private let managedObjectContext: NSManagedObjectContext
2023-03-01 00:48:36 -05:00
2023-03-01 19:09:33 -05:00
public var presentsFinishedNotification: Bool = false
2023-03-01 00:48:36 -05:00
private let refreshIdentifier: String = UUID().uuidString
private var runningApplications: Set<String> = []
2023-03-01 00:48:36 -05:00
2023-03-01 19:09:33 -05:00
public init(installedApps: [InstalledApp]) {
self.installedApps = installedApps
2023-03-01 00:48:36 -05:00
managedObjectContext = installedApps.compactMap { $0.managedObjectContext }.first ?? DatabaseManager.shared.persistentContainer.newBackgroundContext()
super.init()
}
2023-03-01 00:48:36 -05:00
2023-03-01 19:09:33 -05:00
public override func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>) {
super.finish(result)
2023-03-01 00:48:36 -05:00
scheduleFinishedRefreshingNotification(for: result, delay: 0)
managedObjectContext.perform {
self.stopListeningForRunningApps()
}
2023-03-01 00:48:36 -05:00
DispatchQueue.main.async {
2023-03-01 00:48:36 -05:00
if UIApplication.shared.applicationState == .background {}
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
}
}
2023-03-01 00:48:36 -05:00
2023-03-01 19:09:33 -05:00
public override func main() {
super.main()
2023-03-01 00:48:36 -05:00
guard !installedApps.isEmpty else {
finish(.failure(RefreshError.noInstalledApps))
return
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
2023-03-01 00:48:36 -05:00
managedObjectContext.perform {
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
2023-03-01 00:48:36 -05:00
self.startListeningForRunningApps()
2023-03-01 00:48:36 -05:00
// Wait for 3 seconds (2 now, 1 later in FindServerOperation) to:
// a) give us time to discover AltServers
// b) give other processes a chance to respond to requestAppState notification
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.managedObjectContext.perform {
let filteredApps = self.installedApps.filter { !self.runningApplications.contains($0.bundleIdentifier) }
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
2023-03-01 00:48:36 -05:00
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
2023-03-01 00:48:36 -05:00
group.beginInstallationHandler = { installedApp in
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
2023-03-01 00:48:36 -05:00
// We're starting to install AltStore, which means the app is about to quit.
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
// but if the app is still running, we cancel the notification.
// Then, we schedule another notification and repeat the process.
2023-03-01 00:48:36 -05:00
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
2023-03-01 00:48:36 -05:00
if let error = group.context.error {
self.scheduleFinishedRefreshingNotification(for: .failure(error))
2023-03-01 00:48:36 -05:00
} else {
var results = group.results
results[installedApp.bundleIdentifier] = .success(installedApp)
2023-03-01 00:48:36 -05:00
self.scheduleFinishedRefreshingNotification(for: .success(results))
}
}
2023-03-01 00:48:36 -05:00
group.completionHandler = { results in
self.finish(.success(results))
}
}
}
}
}
}
2023-03-01 00:48:36 -05:00
private extension BackgroundRefreshAppsOperation {
func startListeningForRunningApps() {
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
2023-03-01 00:48:36 -05:00
for installedApp in installedApps {
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
2023-03-01 00:48:36 -05:00
let requestAppStateNotification = CFNotificationName.requestAppState(for: installedApp.bundleIdentifier)
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
}
}
2023-03-01 00:48:36 -05:00
func stopListeningForRunningApps() {
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
2023-03-01 00:48:36 -05:00
for installedApp in installedApps {
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
CFNotificationCenterRemoveObserver(notificationCenter, observer, appIsRunningNotification, nil)
}
}
2023-03-01 00:48:36 -05:00
func receivedApplicationState(notification: CFNotificationName) {
let baseName = String(CFNotificationName.appIsRunning.rawValue)
2023-03-01 00:48:36 -05:00
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
2023-03-01 00:48:36 -05:00
runningApplications.insert(appID)
}
2023-03-01 00:48:36 -05:00
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, delay: TimeInterval = 5) {
func scheduleFinishedRefreshingNotification() {
cancelFinishedRefreshingNotification()
let content = UNMutableNotificationContent()
2023-03-01 00:48:36 -05:00
var shouldPresentAlert = false
2023-03-01 00:48:36 -05:00
do {
let results = try result.get()
shouldPresentAlert = false
2023-03-01 00:48:36 -05:00
for (_, result) in results {
guard case let .failure(error) = result else { continue }
throw error
}
2023-03-01 00:48:36 -05:00
content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
2023-03-01 00:48:36 -05:00
} catch RefreshError.noInstalledApps {
shouldPresentAlert = false
2023-03-01 00:48:36 -05:00
} catch {
print("Failed to refresh apps in background.", error)
2023-03-01 00:48:36 -05:00
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
2023-03-01 00:48:36 -05:00
shouldPresentAlert = false
}
2023-03-01 00:48:36 -05:00
if shouldPresentAlert {
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
2023-03-01 00:48:36 -05:00
let request = UNNotificationRequest(identifier: refreshIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
2023-03-01 00:48:36 -05:00
if delay > 0 {
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
2023-03-01 00:48:36 -05:00
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
// If app is still running at this point, we schedule another notification with same identifier.
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
// and we should stop polling.
guard requests.contains(where: { $0.identifier == self.refreshIdentifier }) else { return }
2023-03-01 00:48:36 -05:00
scheduleFinishedRefreshingNotification()
}
}
}
}
}
2023-03-01 00:48:36 -05:00
if presentsFinishedNotification {
scheduleFinishedRefreshingNotification()
2023-03-01 00:48:36 -05:00
}
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
_ = RefreshAttempt(identifier: self.refreshIdentifier, result: result, context: context)
2023-03-01 00:48:36 -05:00
do { try context.save() } catch { print("Failed to save refresh attempt.", error) }
}
}
2023-03-01 00:48:36 -05:00
func cancelFinishedRefreshingNotification() {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [refreshIdentifier])
}
}