2019-06-10 15:03:47 -07:00
//
// I n s t a l l A p p O p e r a t i o n . s w i f t
// A l t S t o r e
//
2019-06-21 11:20:03 -07:00
// C r e a t e d b y R i l e y T e s t u t o n 6 / 1 9 / 1 9 .
2019-06-10 15:03:47 -07:00
// C o p y r i g h t © 2 0 1 9 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
import Network
2020-09-03 16:39:08 -07:00
import AltStoreCore
2019-07-28 15:08:13 -07:00
import AltSign
2019-06-21 11:20:03 -07:00
import Roxas
2019-06-10 15:03:47 -07:00
@objc ( InstallAppOperation )
2019-07-28 15:08:13 -07:00
class InstallAppOperation : ResultOperation < InstalledApp >
2019-06-10 15:03:47 -07:00
{
2020-03-06 17:08:35 -08:00
let context : InstallAppOperationContext
2019-06-10 15:03:47 -07:00
2019-12-11 12:26:48 -08:00
private var didCleanUp = false
2020-03-06 17:08:35 -08:00
init ( context : InstallAppOperationContext )
2019-06-10 15:03:47 -07:00
{
2019-06-21 11:20:03 -07:00
self . context = context
2019-06-10 15:03:47 -07:00
super . init ( )
2019-06-21 11:20:03 -07:00
self . progress . totalUnitCount = 100
2019-06-10 15:03:47 -07:00
}
override func main ( )
{
super . main ( )
2019-06-21 11:20:03 -07:00
if let error = self . context . error
{
self . finish ( . failure ( error ) )
return
}
2019-06-10 15:03:47 -07:00
2019-06-21 11:20:03 -07:00
guard
2020-03-06 17:08:35 -08:00
let certificate = self . context . certificate ,
2019-07-28 15:08:13 -07:00
let resignedApp = self . context . resignedApp ,
2020-01-08 12:41:02 -08:00
let connection = self . context . installationConnection
2019-06-21 11:20:03 -07:00
else { return self . finish ( . failure ( OperationError . invalidParameters ) ) }
2023-10-18 14:06:10 -05:00
Logger . sideload . notice ( " Installing resigned app \( resignedApp . bundleIdentifier , privacy : . public ) ... " )
2023-05-26 19:12:13 -05:00
@ Managed var appVersion = self . context . appVersion
let storeBuildVersion = $ appVersion . buildVersion
2019-07-28 15:08:13 -07:00
let backgroundContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
backgroundContext . perform {
2020-01-24 15:03:16 -08:00
/* A p p */
2019-07-28 15:08:13 -07:00
let installedApp : InstalledApp
// F e t c h + u p d a t e r a t h e r t h a n i n s e r t + r e s o l v e m e r g e c o n f l i c t s t o p r e v e n t p o t e n t i a l c o n t e x t - l e v e l c o n f l i c t s .
if let app = InstalledApp . first ( satisfying : NSPredicate ( format : " %K == %@ " , # keyPath ( InstalledApp . bundleIdentifier ) , self . context . bundleIdentifier ) , in : backgroundContext )
2019-06-10 15:03:47 -07:00
{
2019-07-28 15:08:13 -07:00
installedApp = app
}
else
{
2023-05-26 19:12:13 -05:00
installedApp = InstalledApp ( resignedApp : resignedApp ,
originalBundleIdentifier : self . context . bundleIdentifier ,
certificateSerialNumber : certificate . serialNumber ,
storeBuildVersion : storeBuildVersion ,
context : backgroundContext )
2019-07-28 15:08:13 -07:00
}
2023-05-26 19:12:13 -05:00
installedApp . update ( resignedApp : resignedApp , certificateSerialNumber : certificate . serialNumber , storeBuildVersion : storeBuildVersion )
2020-10-01 11:51:39 -07:00
installedApp . needsResign = false
2020-01-24 15:03:16 -08:00
if let team = DatabaseManager . shared . activeTeam ( in : backgroundContext )
{
installedApp . team = team
}
/* A p p E x t e n s i o n s */
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 }
2020-01-24 15:03:16 -08:00
let parentBundleID = self . context . bundleIdentifier
let resignedParentBundleID = resignedApp . bundleIdentifier
let resignedBundleID = appExtension . bundleIdentifier
let originalBundleID = resignedBundleID . replacingOccurrences ( of : resignedParentBundleID , with : parentBundleID )
2020-01-21 16:53:34 -08:00
let installedExtension : InstalledExtension
2020-01-24 15:03:16 -08:00
if let appExtension = installedApp . appExtensions . first ( where : { $0 . bundleIdentifier = = originalBundleID } )
2020-01-21 16:53:34 -08:00
{
installedExtension = appExtension
}
else
{
2020-01-24 15:03:16 -08:00
installedExtension = InstalledExtension ( resignedAppExtension : appExtension , originalBundleIdentifier : originalBundleID , context : backgroundContext )
2020-01-21 16:53:34 -08:00
}
2020-01-24 15:03:16 -08:00
installedExtension . update ( resignedAppExtension : appExtension )
2020-01-21 16:53:34 -08:00
installedExtensions . insert ( installedExtension )
}
}
installedApp . appExtensions = installedExtensions
2021-10-25 22:27:30 -07:00
self . context . beginInstallationHandler ? ( installedApp )
2019-12-11 12:26:48 -08:00
// T e m p o r a r y d i r e c t o r y a n d r e s i g n e d . i p a n o l o n g e r n e e d e d , s o d e l e t e t h e m n o w t o e n s u r e A l t S t o r e d o e s n ' t q u i t b e f o r e w e g e t t h e c h a n c e t o .
self . cleanUp ( )
2020-03-11 14:43:19 -07:00
var activeProfiles : Set < String > ?
if let sideloadedAppsLimit = UserDefaults . standard . activeAppsLimit
{
// W h e n i n s t a l l i n g t h e s e n e w p r o f i l e s , A l t S e r v e r w i l l r e m o v e a l l n o n - a c t i v e p r o f i l e s t o e n s u r e w e r e m a i n u n d e r l i m i t .
let fetchRequest = InstalledApp . activeAppsFetchRequest ( )
fetchRequest . includesPendingChanges = false
var activeApps = InstalledApp . fetch ( fetchRequest , in : backgroundContext )
if ! activeApps . contains ( installedApp )
{
2020-05-17 23:36:30 -07:00
let activeAppsCount = activeApps . map { $0 . requiredActiveSlots } . reduce ( 0 , + )
2020-03-11 14:43:19 -07:00
2020-05-17 23:36:30 -07:00
let availableActiveApps = max ( sideloadedAppsLimit - activeAppsCount , 0 )
if installedApp . requiredActiveSlots <= availableActiveApps
2020-03-11 14:43:19 -07:00
{
// T h i s a p p h a s n o t b e e n e x p l i c i t l y a c t i v a t e d , b u t t h e r e a r e e n o u g h s l o t s a v a i l a b l e ,
// s o i m p l i c i t l y a c t i v a t e i t .
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
} )
}
2023-10-18 14:06:10 -05:00
let resignedBundleID = installedApp . resignedBundleIdentifier
let request = BeginInstallationRequest ( activeProfiles : activeProfiles , bundleIdentifier : resignedBundleID )
2020-01-08 12:41:02 -08:00
connection . send ( request ) { ( result ) in
2019-07-28 15:08:13 -07:00
switch result
{
2023-10-18 14:06:10 -05:00
case . failure ( let error ) :
Logger . sideload . notice ( " Failed to send begin installation request for resigned app \( resignedBundleID , privacy : . public ) . \( error . localizedDescription , privacy : . public ) " )
self . finish ( . failure ( error ) )
2019-07-28 15:08:13 -07:00
case . success :
2023-10-18 14:06:10 -05:00
Logger . sideload . notice ( " Sent begin installation request for resigned app \( resignedBundleID , privacy : . public ) . " )
2019-07-28 15:08:13 -07:00
2020-01-08 12:41:02 -08:00
self . receive ( from : connection ) { ( result ) in
2019-07-28 15:08:13 -07:00
switch result
{
case . success :
backgroundContext . perform {
2023-10-18 14:06:10 -05:00
Logger . sideload . notice ( " Successfully installed resigned app \( resignedBundleID , privacy : . public ) ! " )
2019-07-28 15:08:13 -07:00
installedApp . refreshedDate = Date ( )
self . finish ( . success ( installedApp ) )
}
case . failure ( let error ) :
2023-10-18 14:06:10 -05:00
Logger . sideload . notice ( " Failed to install resigned app \( resignedBundleID , privacy : . public ) . \( error . localizedDescription , privacy : . public ) " )
2019-07-28 15:08:13 -07:00
self . finish ( . failure ( error ) )
2019-07-19 16:42:40 -07:00
}
}
2019-06-10 15:03:47 -07:00
}
}
}
}
2019-12-11 12:26:48 -08:00
override func finish ( _ result : Result < InstalledApp , Error > )
{
self . cleanUp ( )
2020-06-04 19:53:10 -07:00
// O n l y r e m o v e r e f r e s h e d I P A w h e n f i n i s h e d .
if let app = self . context . app
{
let fileURL = InstalledApp . refreshedIPAURL ( for : app )
do
{
try FileManager . default . removeItem ( at : fileURL )
}
catch
{
2023-10-18 14:06:10 -05:00
Logger . sideload . error ( " Failed to remove refreshed .ipa: \( error . localizedDescription , privacy : . public ) " )
2020-06-04 19:53:10 -07:00
}
}
2019-12-11 12:26:48 -08:00
super . finish ( result )
}
}
private extension InstallAppOperation
{
2020-01-08 12:41:02 -08:00
func receive ( from connection : ServerConnection , completionHandler : @ escaping ( Result < Void , Error > ) -> Void )
2019-06-10 15:03:47 -07:00
{
2020-01-08 12:41:02 -08:00
connection . receiveResponse ( ) { ( result ) in
2019-06-21 11:20:03 -07:00
do
2019-06-10 15:03:47 -07:00
{
2019-06-21 11:20:03 -07:00
let response = try result . get ( )
2023-10-18 14:06:10 -05:00
2019-11-18 14:49:17 -08:00
switch response
2019-06-10 15:03:47 -07:00
{
2019-11-18 14:49:17 -08:00
case . installationProgress ( let response ) :
2023-10-18 14:06:10 -05:00
Logger . sideload . debug ( " Installing \( self . context . resignedApp ? . bundleIdentifier ? ? self . context . bundleIdentifier , privacy : . public ) ... \( ( response . progress * 100 ) . rounded ( ) ) % " )
2019-11-18 14:49:17 -08:00
if response . progress = = 1.0
{
self . progress . completedUnitCount = self . progress . totalUnitCount
completionHandler ( . success ( ( ) ) )
}
else
{
self . progress . completedUnitCount = Int64 ( response . progress * 100 )
2020-01-08 12:41:02 -08:00
self . receive ( from : connection , completionHandler : completionHandler )
2019-11-18 14:49:17 -08:00
}
case . error ( let response ) :
completionHandler ( . failure ( response . error ) )
default :
completionHandler ( . failure ( ALTServerError ( . unknownRequest ) ) )
2019-06-10 15:03:47 -07:00
}
}
catch
{
2019-06-21 11:20:03 -07:00
completionHandler ( . failure ( ALTServerError ( error ) ) )
2019-06-10 15:03:47 -07:00
}
}
}
2019-12-11 12:26:48 -08:00
func cleanUp ( )
{
guard ! self . didCleanUp else { return }
self . didCleanUp = true
do
{
try FileManager . default . removeItem ( at : self . context . temporaryDirectory )
}
catch
{
2023-10-18 14:06:10 -05:00
Logger . sideload . error ( " Failed to remove temporary directory: \( error . localizedDescription , privacy : . public ) " )
2019-12-11 12:26:48 -08:00
}
}
2019-06-10 15:03:47 -07:00
}