mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
* Change error from Swift.Error to NSError
* Adds ResultOperation.localizedFailure
* Finish Riley's monster commit
3b38d725d7
May the Gods have mercy on my soul.
* Fix format strings I broke
* Include "Enable JIT" errors in Error Log
* Fix minimuxer status checking
* [skip ci] Update the no wifi message to include VPN
* Opens Error Log when tapping ToastView
* Fixes Error Log context menu covering cell content
* Fixes Error Log context menu appearing while scrolling
* Fixes incorrect Search FAQ URL
* Fix Error Log showing UIAlertController on iOS 14+
* Fix Error Log not showing UIAlertController on iOS <=13
* Fix wrong color in AuthenticationViewController
* Fix typo
* Fixes logging non-AltServerErrors as AltServerError.underlyingError
* Limits quitting other AltStore/SideStore processes to database migrations
* Skips logging cancelled errors
* Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion
We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.
However, we kept the underlying Core Data property name the same to avoid extra migration.
* Conforms OperatingSystemVersion to Comparable
* Parses AppVersion.minOSVersion/maxOSVersion from source JSON
* Supports non-NSManagedObjects for @Managed properties
This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.
* Supports optional @Managed properties
* Conforms AppVersion to AppProtocol
* Verifies min/max OS version before downloading app + asks user to download older app version if necessary
* Improves error message when file does not exist at AppVersion.downloadURL
* Removes unnecessary StoreApp convenience properties
* Removes unnecessary StoreApp convenience properties as well as fix other issues
* Remove Settings bundle, add SwiftUI view instead
Fix refresh all shortcut intent
* Update AuthenticationOperation.swift
Signed-off-by: June Park <rjp2030@outlook.com>
* Fix build issues given by develop
* Add availability check to fix CI build(?)
* If it's gonna be that way...
---------
Signed-off-by: June Park <rjp2030@outlook.com>
Co-authored-by: nythepegasus <nythepegasus84@gmail.com>
Co-authored-by: Riley Testut <riley@rileytestut.com>
Co-authored-by: ny <me@nythepegas.us>
278 lines
11 KiB
Swift
278 lines
11 KiB
Swift
//
|
|
// BackgroundRefreshAppsOperation.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 7/6/20.
|
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import CoreData
|
|
|
|
import AltStoreCore
|
|
import EmotionalDamage
|
|
import minimuxer
|
|
|
|
typealias RefreshError = RefreshErrorCode.Error
|
|
enum RefreshErrorCode: Int, ALTErrorEnum, CaseIterable
|
|
{
|
|
case noInstalledApps
|
|
|
|
var errorFailureReason: String {
|
|
switch self
|
|
{
|
|
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension CFNotificationName
|
|
{
|
|
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
|
|
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
|
|
|
|
static func requestAppState(for appID: String) -> CFNotificationName
|
|
{
|
|
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
|
|
return CFNotificationName(name as CFString)
|
|
}
|
|
|
|
static func appIsRunning(for appID: String) -> CFNotificationName
|
|
{
|
|
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
|
|
return CFNotificationName(name as CFString)
|
|
}
|
|
}
|
|
|
|
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
|
{ (center, observer, name, object, userInfo) in
|
|
guard let name = name, let observer = observer else { return }
|
|
|
|
let operation = unsafeBitCast(observer, to: BackgroundRefreshAppsOperation.self)
|
|
operation.receivedApplicationState(notification: name)
|
|
}
|
|
|
|
@objc(BackgroundRefreshAppsOperation)
|
|
final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
|
|
{
|
|
let installedApps: [InstalledApp]
|
|
private let managedObjectContext: NSManagedObjectContext
|
|
|
|
var presentsFinishedNotification: Bool = false
|
|
|
|
private let refreshIdentifier: String = UUID().uuidString
|
|
private var runningApplications: Set<String> = []
|
|
|
|
init(installedApps: [InstalledApp])
|
|
{
|
|
self.installedApps = installedApps
|
|
self.managedObjectContext = installedApps.compactMap({ $0.managedObjectContext }).first ?? DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
|
|
|
super.init()
|
|
}
|
|
|
|
override func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
|
|
{
|
|
super.finish(result)
|
|
|
|
self.scheduleFinishedRefreshingNotification(for: result, delay: 0)
|
|
|
|
self.managedObjectContext.perform {
|
|
self.stopListeningForRunningApps()
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
if UIApplication.shared.applicationState == .background
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
override func main()
|
|
{
|
|
super.main()
|
|
|
|
guard !self.installedApps.isEmpty else {
|
|
self.finish(.failure(RefreshError(.noInstalledApps)))
|
|
return
|
|
}
|
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
|
target_minimuxer_address()
|
|
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
|
|
do {
|
|
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
|
|
} catch {
|
|
self.finish(.failure(error))
|
|
}
|
|
if #available(iOS 17, *) {
|
|
// TODO: iOS 17 and above have a new JIT implementation that is completely broken in SideStore :(
|
|
} else {
|
|
start_auto_mounter(documentsDirectory)
|
|
}
|
|
|
|
self.managedObjectContext.perform {
|
|
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
|
|
|
self.startListeningForRunningApps()
|
|
|
|
// 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 })
|
|
|
|
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
|
|
group.beginInstallationHandler = { (installedApp) in
|
|
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
|
|
|
|
// 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.
|
|
|
|
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
|
|
|
|
if let error = group.context.error
|
|
{
|
|
self.scheduleFinishedRefreshingNotification(for: .failure(error))
|
|
}
|
|
else
|
|
{
|
|
var results = group.results
|
|
results[installedApp.bundleIdentifier] = .success(installedApp)
|
|
|
|
self.scheduleFinishedRefreshingNotification(for: .success(results))
|
|
}
|
|
}
|
|
group.completionHandler = { (results) in
|
|
self.finish(.success(results))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension BackgroundRefreshAppsOperation
|
|
{
|
|
func startListeningForRunningApps()
|
|
{
|
|
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
|
let observer = Unmanaged.passUnretained(self).toOpaque()
|
|
|
|
for installedApp in self.installedApps
|
|
{
|
|
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
|
|
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
|
|
|
|
let requestAppStateNotification = CFNotificationName.requestAppState(for: installedApp.bundleIdentifier)
|
|
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
|
|
}
|
|
}
|
|
|
|
func stopListeningForRunningApps()
|
|
{
|
|
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
|
let observer = Unmanaged.passUnretained(self).toOpaque()
|
|
|
|
for installedApp in self.installedApps
|
|
{
|
|
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
|
|
CFNotificationCenterRemoveObserver(notificationCenter, observer, appIsRunningNotification, nil)
|
|
}
|
|
}
|
|
|
|
func receivedApplicationState(notification: CFNotificationName)
|
|
{
|
|
let baseName = String(CFNotificationName.appIsRunning.rawValue)
|
|
|
|
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
|
|
self.runningApplications.insert(appID)
|
|
}
|
|
|
|
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, delay: TimeInterval = 5)
|
|
{
|
|
func scheduleFinishedRefreshingNotification()
|
|
{
|
|
self.cancelFinishedRefreshingNotification()
|
|
|
|
let content = UNMutableNotificationContent()
|
|
|
|
var shouldPresentAlert = true
|
|
|
|
do
|
|
{
|
|
let results = try result.get()
|
|
shouldPresentAlert = false
|
|
|
|
for (_, result) in results
|
|
{
|
|
guard case let .failure(error) = result else { continue }
|
|
throw error
|
|
}
|
|
|
|
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
|
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
|
}
|
|
catch ~OperationError.Code.noWiFi, ~RefreshErrorCode.noInstalledApps
|
|
{
|
|
shouldPresentAlert = false
|
|
}
|
|
catch
|
|
{
|
|
print("Failed to refresh apps in background.", error)
|
|
|
|
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
|
content.body = error.localizedDescription
|
|
}
|
|
|
|
if shouldPresentAlert
|
|
{
|
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
|
|
|
let request = UNNotificationRequest(identifier: self.refreshIdentifier, content: content, trigger: trigger)
|
|
UNUserNotificationCenter.current().add(request)
|
|
|
|
if delay > 0
|
|
{
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
|
|
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 }
|
|
|
|
scheduleFinishedRefreshingNotification()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.presentsFinishedNotification
|
|
{
|
|
scheduleFinishedRefreshingNotification()
|
|
}
|
|
|
|
// 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)
|
|
|
|
do { try context.save() }
|
|
catch { print("Failed to save refresh attempt.", error) }
|
|
}
|
|
}
|
|
|
|
func cancelFinishedRefreshingNotification()
|
|
{
|
|
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [self.refreshIdentifier])
|
|
}
|
|
}
|