Refreshes apps by installing provisioning profiles when possible

Assuming the certificate used to originally sign an app is still valid, we can refresh an app simply by installing new provisioning profiles. However, if the signing certificate is no longer valid, we fall back to the old method of resigning + reinstalling.
This commit is contained in:
Riley Testut
2020-03-06 17:08:35 -08:00
parent 27bce4e456
commit 4f00018164
25 changed files with 1272 additions and 1082 deletions

View File

@@ -34,6 +34,8 @@
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; }; BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; };
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; }; BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; }; BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */; };
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; };
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; }; BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; }; BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; };
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; }; BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; };
@@ -127,9 +129,9 @@
BF718BD823C93DB700A89F2D /* AltKit.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD723C93DB700A89F2D /* AltKit.m */; }; BF718BD823C93DB700A89F2D /* AltKit.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD723C93DB700A89F2D /* AltKit.m */; };
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */; }; BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */; };
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; }; BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; };
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; }; BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* OperationContexts.swift */; };
BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; }; BF770E5622BC3C03002A40FE /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5522BC3C02002A40FE /* Server.swift */; };
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; }; BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */; };
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; }; BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; };
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; }; BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; };
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */; }; BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */; };
@@ -146,7 +148,6 @@
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172823C56042001B5953 /* ServerConnection.swift */; }; BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172823C56042001B5953 /* ServerConnection.swift */; };
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; }; BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; }; BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; };
BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */; };
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; }; BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; }; BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; }; BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; };
@@ -227,7 +228,6 @@
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; }; BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; }; BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; }; BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */; };
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE944023F22AA100CDA07D /* AppIDComponents.swift */; }; BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE944023F22AA100CDA07D /* AppIDComponents.swift */; };
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; }; BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; };
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; }; BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
@@ -332,6 +332,8 @@
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; }; BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = "<group>"; }; BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = "<group>"; };
BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = "<group>"; }; BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = "<group>"; };
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchProvisioningProfilesOperation.swift; sourceTree = "<group>"; };
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAppOperation.swift; sourceTree = "<group>"; };
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; }; BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; }; BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; }; BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
@@ -443,9 +445,9 @@
BF718BD723C93DB700A89F2D /* AltKit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AltKit.m; sourceTree = "<group>"; }; BF718BD723C93DB700A89F2D /* AltKit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AltKit.m; sourceTree = "<group>"; };
BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = "<group>"; }; BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = "<group>"; };
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; }; BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = "<group>"; };
BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = "<group>"; }; BF770E5322BC044E002A40FE /* OperationContexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationContexts.swift; sourceTree = "<group>"; };
BF770E5522BC3C02002A40FE /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; }; BF770E5522BC3C02002A40FE /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = "<group>"; }; BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshGroup.swift; sourceTree = "<group>"; };
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = "<group>"; }; BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = "<group>"; };
BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = "<group>"; }; BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = "<group>"; };
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 3.xcdatamodel"; sourceTree = "<group>"; }; BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 3.xcdatamodel"; sourceTree = "<group>"; };
@@ -464,7 +466,6 @@
BFA8172823C56042001B5953 /* ServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnection.swift; sourceTree = "<group>"; }; BFA8172823C56042001B5953 /* ServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConnection.swift; sourceTree = "<group>"; };
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = "<group>"; }; BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = "<group>"; };
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledExtension.swift; sourceTree = "<group>"; }; BFA8172C23C5823E001B5953 /* InstalledExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledExtension.swift; sourceTree = "<group>"; };
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrepareDeveloperAccountOperation.swift; sourceTree = "<group>"; };
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; }; BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; }; BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; }; BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; };
@@ -552,7 +553,6 @@
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; }; BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; }; BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; }; BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; };
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTApplication+AppExtensions.swift"; sourceTree = "<group>"; };
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = "<group>"; }; BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = "<group>"; };
BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = "<group>"; }; BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = "<group>"; };
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = "<group>"; }; BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = "<group>"; };
@@ -1098,7 +1098,6 @@
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */, BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */, BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */, BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
BFEE943E23F21BD800CDA07D /* ALTApplication+AppExtensions.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1161,13 +1160,14 @@
children = ( children = (
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */, BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */,
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */, BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */, BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */,
BF770E5322BC044E002A40FE /* AppOperationContext.swift */, BF770E5322BC044E002A40FE /* OperationContexts.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */, BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */, BFB364592325985F00CD0EB1 /* FindServerOperation.swift */,
BFA8172E23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */, BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */,
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */, BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
@@ -1681,6 +1681,7 @@
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */, BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */,
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */, BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */, BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */, BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */,
@@ -1689,7 +1690,7 @@
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */, BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */, BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */, BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */,
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
@@ -1697,7 +1698,6 @@
BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */, BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */,
BFEE943F23F21BD800CDA07D /* ALTApplication+AppExtensions.swift in Sources */,
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */,
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */, BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
@@ -1721,10 +1721,9 @@
BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */, BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */,
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */,
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */, BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */,
BFA8172F23C5831A001B5953 /* PrepareDeveloperAccountOperation.swift in Sources */,
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */, BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */,
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */, BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */, BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */, BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */, BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
@@ -1751,6 +1750,7 @@
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */, BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */, BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
BF770E5622BC3C03002A40FE /* Server.swift in Sources */, BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */, BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */,

View File

@@ -64,6 +64,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
private var runningApplications: Set<String>? private var runningApplications: Set<String>?
private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{ {
@@ -209,6 +210,8 @@ extension AppDelegate
} }
taskCompletionHandler() taskCompletionHandler()
self.backgroundRefreshContext = nil
} }
if let error = taskResult.error if let error = taskResult.error
@@ -339,7 +342,6 @@ private extension AppDelegate
dispatchGroup.enter() dispatchGroup.enter()
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context) let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
guard !installedApps.isEmpty else { guard !installedApps.isEmpty else {
serversResult = .success(()) serversResult = .success(())
@@ -351,6 +353,7 @@ private extension AppDelegate
} }
self.runningApplications = [] self.runningApplications = []
self.backgroundRefreshContext = context
let identifiers = installedApps.compactMap { $0.bundleIdentifier } let identifiers = installedApps.compactMap { $0.bundleIdentifier }
print("Apps to refresh:", identifiers) print("Apps to refresh:", identifiers)
@@ -398,7 +401,7 @@ private extension AppDelegate
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background. // 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.error if let error = group.context.error
{ {
self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier) self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier)
} }
@@ -410,8 +413,8 @@ private extension AppDelegate
self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier) self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier)
} }
} }
group.completionHandler = { (result) in group.completionHandler = { (results) in
completionHandler(result) completionHandler(.success(results))
} }
} }
} }

View File

@@ -13,8 +13,7 @@ import Roxas
class RefreshAltStoreViewController: UIViewController class RefreshAltStoreViewController: UIViewController
{ {
var signer: ALTSigner! var context: AuthenticatedOperationContext!
var session: ALTAppleAPISession!
var completionHandler: ((Result<Void, Error>) -> Void)? var completionHandler: ((Result<Void, Error>) -> Void)?
@@ -42,18 +41,18 @@ private extension RefreshAltStoreViewController
{ {
sender.isIndicatingActivity = true sender.isIndicatingActivity = true
if let progress = AppManager.shared.refreshProgress(for: altStore) ?? AppManager.shared.installationProgress(for: altStore) if let progress = AppManager.shared.installationProgress(for: altStore)
{ {
// Cancel pending AltStore refresh so we can start a new one. // Cancel pending AltStore installation so we can start a new one.
progress.cancel() progress.cancel()
} }
let group = OperationGroup() // Install, _not_ refresh, to ensure we are installing with a non-revoked certificate.
group.signer = self.signer // Prevent us from trying to authenticate a second time. let progress = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in
group.session = self.session // ^ switch result
group.completionHandler = { (result) in
if let error = result.error ?? result.value?.values.compactMap({ $0.error }).first
{ {
case .success: self.completionHandler?(.success(()))
case .failure(let error):
DispatchQueue.main.async { DispatchQueue.main.async {
sender.progress = nil sender.progress = nil
sender.isIndicatingActivity = false sender.isIndicatingActivity = false
@@ -69,14 +68,9 @@ private extension RefreshAltStoreViewController
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }
} }
else
{
self.completionHandler?(.success(()))
}
} }
_ = AppManager.shared.refresh([altStore], presentingViewController: self, group: group) sender.progress = progress
sender.progress = group.progress
} }
refresh() refresh()

View File

@@ -1,29 +0,0 @@
//
// ALTApplication+AppExtensions.swift
// AltStore
//
// Created by Riley Testut on 2/10/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import AltSign
extension ALTApplication
{
var appExtensions: Set<ALTApplication> {
guard let bundle = Bundle(url: self.fileURL) else { return [] }
var appExtensions: Set<ALTApplication> = []
if let directory = bundle.builtInPlugInsURL, let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants])
{
for case let fileURL as URL in enumerator where fileURL.pathExtension.lowercased() == "appex"
{
guard let appExtension = ALTApplication(fileURL: fileURL) else { continue }
appExtensions.insert(appExtension)
}
}
return appExtensions
}
}

View File

@@ -30,7 +30,7 @@ class AppManager
static let shared = AppManager() static let shared = AppManager()
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
private let processingQueue = DispatchQueue(label: "com.altstore.AppManager.processingQueue") private let serialOperationQueue = OperationQueue()
private var installationProgress = [String: Progress]() private var installationProgress = [String: Progress]()
private var refreshProgress = [String: Progress]() private var refreshProgress = [String: Progress]()
@@ -38,6 +38,9 @@ class AppManager
private init() private init()
{ {
self.operationQueue.name = "com.altstore.AppManager.operationQueue" self.operationQueue.name = "com.altstore.AppManager.operationQueue"
self.serialOperationQueue.name = "com.altstore.AppManager.serialOperationQueue"
self.serialOperationQueue.maxConcurrentOperationCount = 1
} }
} }
@@ -100,36 +103,47 @@ extension AppManager
} }
@discardableResult @discardableResult
func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTSigner, ALTAppleAPISession), Error>) -> Void) -> OperationGroup func findServer(context: OperationContext = OperationContext(), completionHandler: @escaping (Result<Server, Error>) -> Void) -> FindServerOperation
{ {
let group = OperationGroup() let findServerOperation = FindServerOperation(context: context)
let findServerOperation = FindServerOperation(group: group)
findServerOperation.resultHandler = { (result) in findServerOperation.resultHandler = { (result) in
switch result switch result
{ {
case .failure(let error): group.error = error case .failure(let error): context.error = error
case .success(let server): group.server = server case .success(let server): context.server = server
} }
} }
self.operationQueue.addOperation(findServerOperation)
let authenticationOperation = AuthenticationOperation(group: group, presentingViewController: presentingViewController) self.run([findServerOperation])
return findServerOperation
}
@discardableResult
func authenticate(presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>) -> Void) -> AuthenticationOperation
{
if let operation = context.authenticationOperation
{
return operation
}
let findServerOperation = self.findServer(context: context) { _ in }
let authenticationOperation = AuthenticationOperation(context: context, presentingViewController: presentingViewController)
authenticationOperation.resultHandler = { (result) in authenticationOperation.resultHandler = { (result) in
switch result switch result
{ {
case .failure(let error): group.error = error case .failure(let error): context.error = error
case .success(let signer, let session): case .success: break
group.signer = signer
group.session = session
} }
completionHandler(result) completionHandler(result)
} }
authenticationOperation.addDependency(findServerOperation) authenticationOperation.addDependency(findServerOperation)
self.operationQueue.addOperation(authenticationOperation)
return group self.run([authenticationOperation])
return authenticationOperation
} }
} }
@@ -154,46 +168,30 @@ extension AppManager
NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self) NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self)
} }
} }
self.operationQueue.addOperation(fetchSourceOperation) self.run([fetchSourceOperation])
} }
} }
func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void) func fetchAppIDs(completionHandler: @escaping (Result<([AppID], NSManagedObjectContext), Error>) -> Void)
{ {
var group: OperationGroup! let authenticationOperation = self.authenticate(presentingViewController: nil) { (result) in
group = self.authenticate(presentingViewController: nil) { (result) in print("Authenticated for fetching App IDs with result:", result)
switch result
{
case .failure(let error):
completionHandler(.failure(error))
case .success:
let fetchAppIDsOperation = FetchAppIDsOperation(group: group)
fetchAppIDsOperation.resultHandler = completionHandler
self.operationQueue.addOperation(fetchAppIDsOperation)
}
} }
let fetchAppIDsOperation = FetchAppIDsOperation(context: authenticationOperation.context)
fetchAppIDsOperation.resultHandler = completionHandler
fetchAppIDsOperation.addDependency(authenticationOperation)
self.run([fetchAppIDsOperation])
} }
}
@discardableResult
extension AppManager func install<T: AppProtocol>(_ app: T, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
func install(_ app: AppProtocol, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
if let progress = self.installationProgress(for: app) let group = RefreshGroup(context: context)
{ group.completionHandler = { (results) in
return progress
}
let bundleIdentifier = app.bundleIdentifier
let group = self.install([app], forceDownload: true, presentingViewController: presentingViewController)
group.completionHandler = { (result) in
do do
{ {
self.installationProgress[bundleIdentifier] = nil guard let result = results.values.first else { throw OperationError.unknown }
guard let (_, result) = try result.get().first else { throw OperationError.unknown }
completionHandler(result) completionHandler(result)
} }
catch catch
@@ -202,24 +200,19 @@ extension AppManager
} }
} }
self.installationProgress[bundleIdentifier] = group.progress let operation = AppOperation.install(app)
self.perform([operation], presentingViewController: presentingViewController, group: group)
return group.progress return group.progress
} }
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup @discardableResult
func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: RefreshGroup? = nil) -> RefreshGroup
{ {
let apps = installedApps.filter { self.refreshProgress(for: $0) == nil || self.refreshProgress(for: $0)?.isCancelled == true } let group = group ?? RefreshGroup()
let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group)
for app in apps let operations = installedApps.map { AppOperation.refresh($0) }
{ return self.perform(operations, presentingViewController: presentingViewController, group: group)
guard let progress = group.progress(for: app) else { continue }
self.refreshProgress[app.bundleIdentifier] = progress
}
return group
} }
func installationProgress(for app: AppProtocol) -> Progress? func installationProgress(for app: AppProtocol) -> Progress?
@@ -237,264 +230,202 @@ extension AppManager
private extension AppManager private extension AppManager
{ {
func install(_ apps: [AppProtocol], forceDownload: Bool, presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup enum AppOperation
{ {
// Authenticate -> Download (if necessary) -> Resign -> Send -> Install. case install(AppProtocol)
let group = group ?? OperationGroup() case refresh(AppProtocol)
var operations = [Operation]()
/* Find Server */ var app: AppProtocol {
let findServerOperation = FindServerOperation(group: group) switch self
findServerOperation.resultHandler = { (result) in
switch result
{ {
case .failure(let error): group.error = error case .install(let app), .refresh(let app): return app
case .success(let server): group.server = server
} }
} }
operations.append(findServerOperation)
let authenticationOperation: AuthenticationOperation? var bundleIdentifier: String {
var bundleIdentifier: String!
if group.signer == nil || group.session == nil
{
/* Authenticate */
let operation = AuthenticationOperation(group: group, presentingViewController: presentingViewController)
operation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success(let signer, let session):
group.signer = signer
group.session = session
}
}
operations.append(operation)
operation.addDependency(findServerOperation)
authenticationOperation = operation if let context = (self.app as? NSManagedObject)?.managedObjectContext
}
else
{
authenticationOperation = nil
}
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(group: group)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{ {
case .failure(let error): group.error = error context.performAndWait { bundleIdentifier = self.app.bundleIdentifier }
case .success(let anisetteData): group.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(authenticationOperation ?? findServerOperation)
operations.append(refreshAnisetteDataOperation)
/* Prepare Developer Account */
let prepareDeveloperAccountOperation = PrepareDeveloperAccountOperation(group: group)
prepareDeveloperAccountOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): group.error = error
case .success: break
}
}
prepareDeveloperAccountOperation.addDependency(refreshAnisetteDataOperation)
operations.append(prepareDeveloperAccountOperation)
for app in apps
{
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, group: group)
let progress = Progress.discreteProgress(totalUnitCount: 100)
/* Resign */
let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in
guard let resignedApp = self.process(result, context: context) else { return }
context.resignedApp = resignedApp
}
resignAppOperation.addDependency(prepareDeveloperAccountOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
operations.append(resignAppOperation)
/* Download */
let fileURL = InstalledApp.fileURL(for: app)
var localApp: ALTApplication?
let managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
managedObjectContext.performAndWait {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), context.bundleIdentifier)
if let installedApp = InstalledApp.first(satisfying: predicate, in: managedObjectContext), FileManager.default.fileExists(atPath: fileURL.path), !forceDownload
{
localApp = ALTApplication(fileURL: installedApp.fileURL)
}
}
if let localApp = localApp
{
// Already installed, don't need to download.
// If we don't need to download the app, reduce the total unit count by 40.
progress.totalUnitCount -= 40
context.app = localApp
} }
else else
{ {
// App is not yet installed (or we're forcing it to download a new version), so download it before resigning it. bundleIdentifier = self.app.bundleIdentifier
let downloadOperation = DownloadAppOperation(app: app, context: context)
downloadOperation.resultHandler = { (result) in
guard let app = self.process(result, context: context) else { return }
context.app = app
}
progress.addChild(downloadOperation.progress, withPendingUnitCount: 40)
downloadOperation.addDependency(findServerOperation)
resignAppOperation.addDependency(downloadOperation)
operations.append(downloadOperation)
} }
/* Send */ return bundleIdentifier
let sendAppOperation = SendAppOperation(context: context)
sendAppOperation.resultHandler = { (result) in
guard let installationConnection = self.process(result, context: context) else { return }
context.installationConnection = installationConnection
}
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 10)
sendAppOperation.addDependency(resignAppOperation)
operations.append(sendAppOperation)
let beginInstallationHandler = group.beginInstallationHandler
group.beginInstallationHandler = { (installedApp) in
if installedApp.bundleIdentifier == StoreApp.altstoreAppID
{
self.scheduleExpirationWarningLocalNotification(for: installedApp)
}
beginInstallationHandler?(installedApp)
}
/* Install */
let installOperation = InstallAppOperation(context: context)
installOperation.resultHandler = { (result) in
if let error = result.error
{
context.error = error
}
if let installedApp = result.value
{
if let app = app as? StoreApp, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? StoreApp
{
installedApp.storeApp = storeApp
}
context.installedApp = installedApp
}
self.finishAppOperation(context) // Finish operation no matter what.
}
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
installOperation.addDependency(sendAppOperation)
operations.append(installOperation)
group.set(progress, for: app)
} }
}
@discardableResult
private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) -> RefreshGroup
{
let operations = operations.filter { self.progress(for: $0) == nil || self.progress(for: $0)?.isCancelled == true }
// Refresh anisette data after downloading all apps to prevent session from expiring. for operation in operations
for case let downloadOperation as DownloadAppOperation in operations
{ {
refreshAnisetteDataOperation.addDependency(downloadOperation) let progress = Progress.discreteProgress(totalUnitCount: 100)
self.set(progress, for: operation)
} }
/* Cache App IDs */ /* Authenticate (if necessary) */
let fetchAppIDsOperation = FetchAppIDsOperation(group: group) var authenticationOperation: AuthenticationOperation?
fetchAppIDsOperation.resultHandler = { (result) in if group.context.session == nil
do {
{ authenticationOperation = self.authenticate(presentingViewController: presentingViewController, context: group.context) { (result) in
let (_, context) = try result.get() switch result
try context.save() {
} case .failure(let error): group.context.error = error
catch case .success: break
{ }
print("Failed to fetch App IDs.", error)
} }
} }
operations.forEach { fetchAppIDsOperation.addDependency($0) }
operations.append(fetchAppIDsOperation)
group.addOperations(operations) func performAppOperations()
{
for operation in operations
{
let progress = self.progress(for: operation)
if let progress = progress
{
group.progress.totalUnitCount += 1
group.progress.addChild(progress, withPendingUnitCount: 1)
if group.context.session != nil
{
// Finished authenticating, so increase completed unit count.
progress.completedUnitCount += 20
}
}
switch operation
{
case .refresh(let installedApp as InstalledApp) where installedApp.certificateSerialNumber == group.context.certificate?.serialNumber:
// Refreshing apps, but using same certificate as last time, so we can just refresh provisioning profiles.
let refreshProgress = self._refresh(installedApp, group: group) { (result) in
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
case .refresh(let app), .install(let app):
// Either installing for first time, or refreshing with a different signing certificate,
// so we need to resign the app then install it.
let installProgress = self._install(app, group: group) { (result) in
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(installProgress, withPendingUnitCount: 80)
}
}
}
if let authenticationOperation = authenticationOperation
{
let awaitAuthenticationOperation = BlockOperation {
if let managedObjectContext = operations.lazy.compactMap({ ($0.app as? NSManagedObject)?.managedObjectContext }).first
{
managedObjectContext.perform { performAppOperations() }
}
else
{
performAppOperations()
}
}
awaitAuthenticationOperation.addDependency(authenticationOperation)
self.run([awaitAuthenticationOperation], requiresSerialQueue: true)
}
else
{
performAppOperations()
}
return group return group
} }
@discardableResult func process<T>(_ result: Result<T, Error>, context: AppOperationContext) -> T?
{
do
{
let value = try result.get()
return value
}
catch OperationError.cancelled
{
context.error = OperationError.cancelled
self.finishAppOperation(context)
return nil
}
catch
{
context.error = error
return nil
}
}
func finishAppOperation(_ context: AppOperationContext) private func _install(_ app: AppProtocol, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{ {
self.processingQueue.sync { let progress = Progress.discreteProgress(totalUnitCount: 100)
guard !context.isFinished else { return }
context.isFinished = true let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.beginInstallationHandler = group.beginInstallationHandler
if let progress = self.refreshProgress[context.bundleIdentifier], progress == context.group.progress(forAppWithBundleIdentifier: context.bundleIdentifier)
/* Download */
let downloadOperation = DownloadAppOperation(app: app, context: context)
downloadOperation.resultHandler = { (result) in
switch result
{ {
// Only remove progress if it hasn't been replaced by another one. case .failure(let error): context.error = error
self.refreshProgress[context.bundleIdentifier] = nil case .success(let app): context.app = app
} }
}
if let error = context.error progress.addChild(downloadOperation.progress, withPendingUnitCount: 25)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
switch result
{ {
switch error case .failure(let error): context.error = error
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
}
}
refreshAnisetteDataOperation.addDependency(downloadOperation)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
}
}
fetchProvisioningProfilesOperation.addDependency(refreshAnisetteDataOperation)
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 5)
/* Resign */
let resignAppOperation = ResignAppOperation(context: context)
resignAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let resignedApp): context.resignedApp = resignedApp
}
}
resignAppOperation.addDependency(fetchProvisioningProfilesOperation)
progress.addChild(resignAppOperation.progress, withPendingUnitCount: 20)
/* Send */
let sendAppOperation = SendAppOperation(context: context)
sendAppOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let installationConnection): context.installationConnection = installationConnection
}
}
sendAppOperation.addDependency(resignAppOperation)
progress.addChild(sendAppOperation.progress, withPendingUnitCount: 20)
/* Install */
let installOperation = InstallAppOperation(context: context)
installOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let installedApp):
if let app = app as? StoreApp, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? StoreApp
{ {
case let error as ALTServerError where error.code == .deviceNotFound || error.code == .lostConnection: installedApp.storeApp = storeApp
if let server = context.group.server, server.isPreferred
{
// Preferred server, so report errors normally.
context.group.results[context.bundleIdentifier] = .failure(error)
}
else
{
// Not preferred server, so ignore these specific errors and throw serverNotFound instead.
context.group.results[context.bundleIdentifier] = .failure(ConnectionError.serverNotFound)
}
case let error:
context.group.results[context.bundleIdentifier] = .failure(error)
}
}
else if let installedApp = context.installedApp
{
context.group.results[context.bundleIdentifier] = .success(installedApp)
// Save after each installation.
installedApp.managedObjectContext?.performAndWait {
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
} }
if let index = UserDefaults.standard.legacySideloadedApps?.firstIndex(of: installedApp.bundleIdentifier) if let index = UserDefaults.standard.legacySideloadedApps?.firstIndex(of: installedApp.bundleIdentifier)
@@ -502,21 +433,100 @@ private extension AppManager
// No longer a legacy sideloaded app, so remove it from cached list. // No longer a legacy sideloaded app, so remove it from cached list.
UserDefaults.standard.legacySideloadedApps?.remove(at: index) UserDefaults.standard.legacySideloadedApps?.remove(at: index)
} }
}
print("Finished operation!", context.bundleIdentifier)
if context.group.results.count == context.group.progress.totalUnitCount
{
context.group.completionHandler?(.success(context.group.results))
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() completionHandler(.success(installedApp))
backgroundContext.performAndWait {
guard let altstore = InstalledApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: backgroundContext) else { return }
self.scheduleExpirationWarningLocalNotification(for: altstore)
}
} }
} }
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
installOperation.addDependency(sendAppOperation)
let operations = [downloadOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
group.add(operations)
self.run(operations)
return progress
}
private func _refresh(_ app: InstalledApp, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 100)
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.url)
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
case .failure(let error): context.error = error
case .success(let provisioningProfiles): context.provisioningProfiles = provisioningProfiles
}
}
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
/* Refresh */
let refreshAppOperation = RefreshAppOperation(context: context)
refreshAppOperation.resultHandler = { (result) in
completionHandler(result)
}
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
group.add(operations)
self.run(operations)
return progress
}
func finish(_ operation: AppOperation, result: Result<InstalledApp, Error>, group: RefreshGroup, progress: Progress?)
{
let result = result.mapError { (resultError) -> Error in
guard let error = resultError as? ALTServerError else { return resultError }
switch error.code
{
case .deviceNotFound, .lostConnection:
if let server = group.context.server, server.isPreferred || server.isWiredConnection
{
// Preferred server (or wired connection), so report errors normally.
return error
}
else
{
// Not preferred server, so ignore these specific errors and throw serverNotFound instead.
return ConnectionError.serverNotFound
}
default: return error
}
}
// Must remove before saving installedApp.
if let currentProgress = self.progress(for: operation), currentProgress == progress
{
// Only remove progress if it hasn't been replaced by another one.
self.set(nil, for: operation)
}
do
{
let installedApp = try result.get()
group.set(.success(installedApp), forAppWithBundleIdentifier: installedApp.bundleIdentifier)
if installedApp.bundleIdentifier == StoreApp.altstoreAppID
{
self.scheduleExpirationWarningLocalNotification(for: installedApp)
}
do { try installedApp.managedObjectContext?.save() }
catch { print("Error saving installed app.", error) }
}
catch
{
group.set(.failure(error), forAppWithBundleIdentifier: operation.bundleIdentifier)
}
} }
func scheduleExpirationWarningLocalNotification(for app: InstalledApp) func scheduleExpirationWarningLocalNotification(for app: InstalledApp)
@@ -539,4 +549,44 @@ private extension AppManager
let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger) let request = UNNotificationRequest(identifier: AppManager.expirationWarningNotificationID, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) UNUserNotificationCenter.current().add(request)
} }
func run(_ operations: [Foundation.Operation], requiresSerialQueue: Bool = false)
{
for operation in operations
{
switch operation
{
case _ where requiresSerialQueue: fallthrough
case is InstallAppOperation, is RefreshAppOperation:
if let previousOperation = self.serialOperationQueue.operations.last
{
// Ensure operations execute in the order they're added, since they may become ready at different points.
operation.addDependency(previousOperation)
}
self.serialOperationQueue.addOperation(operation)
default:
self.operationQueue.addOperation(operation)
}
}
}
func progress(for operation: AppOperation) -> Progress?
{
switch operation
{
case .install: return self.installationProgress[operation.bundleIdentifier]
case .refresh: return self.refreshProgress[operation.bundleIdentifier]
}
}
func set(_ progress: Progress?, for operation: AppOperation)
{
switch operation
{
case .install: self.installationProgress[operation.bundleIdentifier] = progress
case .refresh: self.refreshProgress[operation.bundleIdentifier] = progress
}
}
} }

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16117.1" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="Account" syncable="YES"> <entity name="Account" representedClassName="Account" syncable="YES">
<attribute name="appleID" attributeType="String"/> <attribute name="appleID" attributeType="String"/>
<attribute name="firstName" attributeType="String"/> <attribute name="firstName" attributeType="String"/>
@@ -33,6 +33,7 @@
</entity> </entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES"> <entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String"/> <attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/> <attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/> <attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/> <attribute name="name" attributeType="String"/>

View File

@@ -158,6 +158,7 @@ private extension DatabaseManager
storeApp.source = altStoreSource storeApp.source = altStoreSource
} }
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
let installedApp: InstalledApp let installedApp: InstalledApp
if let app = storeApp.installedApp if let app = storeApp.installedApp
@@ -166,7 +167,7 @@ private extension DatabaseManager
} }
else else
{ {
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, context: context) installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
installedApp.storeApp = storeApp installedApp.storeApp = storeApp
} }
@@ -194,7 +195,7 @@ private extension DatabaseManager
} }
// Must go after comparing versions to see if we need to update our cached AltStore app bundle. // Must go after comparing versions to see if we need to update our cached AltStore app bundle.
installedApp.update(resignedApp: localApp) installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
do do
{ {

View File

@@ -36,6 +36,8 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
@NSManaged var expirationDate: Date @NSManaged var expirationDate: Date
@NSManaged var installedDate: Date @NSManaged var installedDate: Date
@NSManaged var certificateSerialNumber: String?
/* Relationships */ /* Relationships */
@NSManaged var storeApp: StoreApp? @NSManaged var storeApp: StoreApp?
@NSManaged var team: Team? @NSManaged var team: Team?
@@ -50,7 +52,7 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
} }
init(resignedApp: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext) init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext)
{ {
super.init(entity: InstalledApp.entity(), insertInto: context) super.init(entity: InstalledApp.entity(), insertInto: context)
@@ -61,22 +63,29 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile. self.expirationDate = self.refreshedDate.addingTimeInterval(60 * 60 * 24 * 7) // Rough estimate until we get real values from provisioning profile.
self.update(resignedApp: resignedApp) self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
} }
func update(resignedApp: ALTApplication) func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
{ {
self.name = resignedApp.name self.name = resignedApp.name
self.resignedBundleIdentifier = resignedApp.bundleIdentifier self.resignedBundleIdentifier = resignedApp.bundleIdentifier
self.version = resignedApp.version self.version = resignedApp.version
self.certificateSerialNumber = certificateSerialNumber
if let provisioningProfile = resignedApp.provisioningProfile if let provisioningProfile = resignedApp.provisioningProfile
{ {
self.refreshedDate = provisioningProfile.creationDate self.update(provisioningProfile: provisioningProfile)
self.expirationDate = provisioningProfile.expirationDate
} }
} }
func update(provisioningProfile: ALTProvisioningProfile)
{
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
}
} }
extension InstalledApp extension InstalledApp
@@ -153,7 +162,7 @@ extension InstalledApp
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.refreshedDate < date
{ {
// Refresh AltStore last since it causes app to quit. // Refresh AltStore last since it may cause app to quit.
installedApps.append(altStoreApp) installedApps.append(altStoreApp)
} }

View File

@@ -55,10 +55,15 @@ class InstalledExtension: NSManagedObject, InstalledAppProtocol
if let provisioningProfile = resignedAppExtension.provisioningProfile if let provisioningProfile = resignedAppExtension.provisioningProfile
{ {
self.refreshedDate = provisioningProfile.creationDate self.update(provisioningProfile: provisioningProfile)
self.expirationDate = provisioningProfile.expirationDate
} }
} }
func update(provisioningProfile: ALTProvisioningProfile)
{
self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate
}
} }
extension InstalledExtension extension InstalledExtension

View File

@@ -42,7 +42,7 @@ class MyAppsViewController: UICollectionViewController
private var isUpdateSectionCollapsed = true private var isUpdateSectionCollapsed = true
private var expandedAppUpdates = Set<String>() private var expandedAppUpdates = Set<String>()
private var isRefreshingAllApps = false private var isRefreshingAllApps = false
private var refreshGroup: OperationGroup? private var refreshGroup: RefreshGroup?
private var sideloadingProgress: Progress? private var sideloadingProgress: Progress?
// Cache // Cache
@@ -313,7 +313,7 @@ private extension MyAppsViewController
default: cell.bannerView.button.tintColor = .refreshRed default: cell.bannerView.button.tintColor = .refreshRed
} }
if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: installedApp), progress.fractionCompleted < 1.0 if let progress = AppManager.shared.refreshProgress(for: installedApp), progress.fractionCompleted < 1.0
{ {
cell.bannerView.button.progress = progress cell.bannerView.button.progress = progress
} }
@@ -398,85 +398,58 @@ private extension MyAppsViewController
} }
} }
func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping (Result<[String : Result<InstalledApp, Error>], Error>) -> Void) func refresh(_ installedApps: [InstalledApp], completionHandler: @escaping ([String : Result<InstalledApp, Error>]) -> Void)
{ {
func refresh() let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup)
{ group.completionHandler = { (results) in
let group = AppManager.shared.refresh(installedApps, presentingViewController: self, group: self.refreshGroup) DispatchQueue.main.async {
group.completionHandler = { (result) in let failures = results.compactMapValues { (result) -> Error? in
DispatchQueue.main.async {
switch result switch result
{ {
case .failure(let error): case .failure(OperationError.cancelled): return nil
let toastView = ToastView(error: error) case .failure(let error): return error
toastView.show(in: self) case .success: return nil
}
case .success(let results): }
let failures = results.compactMapValues { (result) -> Error? in
switch result guard !failures.isEmpty else { return }
{
case .failure(OperationError.cancelled): return nil let toastView: ToastView
case .failure(let error): return error
case .success: return nil if let failure = failures.first, results.count == 1
} {
} toastView = ToastView(error: failure.value)
}
guard !failures.isEmpty else { break } else
{
let toastView: ToastView let localizedText: String
if let failure = failures.first, results.count == 1 if failures.count == 1
{ {
toastView = ToastView(error: failure.value) localizedText = NSLocalizedString("Failed to refresh 1 app.", comment: "")
} }
else else
{ {
let localizedText: String localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
if failures.count == 1
{
localizedText = NSLocalizedString("Failed to refresh 1 app.", comment: "")
}
else
{
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
}
let detailText = failures.first?.value.localizedDescription
toastView = ToastView(text: localizedText, detailText: detailText)
toastView.preferredDuration = 2.0
}
toastView.show(in: self)
} }
self.refreshGroup = nil let detailText = failures.first?.value.localizedDescription
completionHandler(result)
toastView = ToastView(text: localizedText, detailText: detailText)
toastView.preferredDuration = 2.0
} }
toastView.show(in: self)
} }
self.refreshGroup = group self.refreshGroup = nil
completionHandler(results)
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue))
}
} }
if installedApps.contains(where: { $0.bundleIdentifier == StoreApp.altstoreAppID }) self.refreshGroup = group
{
let alertController = UIAlertController(title: NSLocalizedString("Refresh AltStore?", comment: ""), message: NSLocalizedString("AltStore will quit when it is finished refreshing.", comment: ""), preferredStyle: .alert) UIView.performWithoutAnimation {
alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in self.collectionView.reloadSections([Section.installedApps.rawValue])
completionHandler(.failure(OperationError.cancelled))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .default) { (action) in
refresh()
})
self.present(alertController, animated: true, completion: nil)
}
else
{
refresh()
} }
} }
} }
@@ -559,16 +532,16 @@ private extension MyAppsViewController
return return
} }
self.refresh([installedApp]) { (result) in self.refresh([installedApp]) { (results) in
// If an error occured, reload the section so the progress bar is no longer visible. // If an error occured, reload the section so the progress bar is no longer visible.
if result.error != nil || result.value?.values.contains(where: { $0.error != nil }) == true if results.values.contains(where: { $0.error != nil })
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue)) self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue))
} }
} }
print("Finished refreshing with result:", result.error?.localizedDescription ?? "success") print("Finished refreshing with results:", results.map { ($0, $1.error?.localizedDescription ?? "success") })
} }
} }

View File

@@ -1,58 +0,0 @@
//
// Contexts.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Network
import AltSign
class AppOperationContext
{
lazy var temporaryDirectory: URL = {
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
catch { self.error = error }
return temporaryDirectory
}()
var bundleIdentifier: String
var group: OperationGroup
var app: ALTApplication?
var resignedApp: ALTApplication?
var installationConnection: ServerConnection?
var installedApp: InstalledApp? {
didSet {
self.installedAppContext = self.installedApp?.managedObjectContext
}
}
private var installedAppContext: NSManagedObjectContext?
var isFinished = false
var error: Error? {
get {
return _error ?? self.group.error
}
set {
_error = newValue
}
}
private var _error: Error?
init(bundleIdentifier: String, group: OperationGroup)
{
self.bundleIdentifier = bundleIdentifier
self.group = group
}
}

View File

@@ -32,9 +32,9 @@ enum AuthenticationError: LocalizedError
} }
@objc(AuthenticationOperation) @objc(AuthenticationOperation)
class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)> class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)>
{ {
let group: OperationGroup let context: AuthenticatedOperationContext
private weak var presentingViewController: UIViewController? private weak var presentingViewController: UIViewController?
@@ -56,22 +56,23 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
private var submitCodeAction: UIAlertAction? private var submitCodeAction: UIAlertAction?
init(group: OperationGroup, presentingViewController: UIViewController?) init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?)
{ {
self.group = group self.context = context
self.presentingViewController = presentingViewController self.presentingViewController = presentingViewController
super.init() super.init()
self.context.authenticationOperation = self
self.operationQueue.name = "com.altstore.AuthenticationOperation" self.operationQueue.name = "com.altstore.AuthenticationOperation"
self.progress.totalUnitCount = 3 self.progress.totalUnitCount = 4
} }
override func main() override func main()
{ {
super.main() super.main()
if let error = self.group.error if let error = self.context.error
{ {
self.finish(.failure(error)) self.finish(.failure(error))
return return
@@ -85,6 +86,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let account, let session): case .success(let account, let session):
self.context.session = session
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
// Fetch Team // Fetch Team
@@ -95,6 +97,7 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let team): case .success(let team):
self.context.team = team
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
// Fetch Certificate // Fetch Certificate
@@ -105,22 +108,33 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success(let certificate): case .success(let certificate):
self.context.certificate = certificate
self.progress.completedUnitCount += 1 self.progress.completedUnitCount += 1
// Save account/team to disk. // Register Device
self.save(team) { (result) in self.registerCurrentDevice(for: team, session: session) { (result) in
guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
switch result switch result
{ {
case .failure(let error): self.finish(.failure(error)) case .failure(let error): self.finish(.failure(error))
case .success: case .success:
let signer = ALTSigner(team: team, certificate: certificate) self.progress.completedUnitCount += 1
// Must cache App IDs _after_ saving account/team to disk. // Save account/team to disk.
self.cacheAppIDs(signer: signer, session: session) { (result) in self.save(team) { (result) in
let result = result.map { _ in (signer, session) } guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) }
self.finish(result)
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
// Must cache App IDs _after_ saving account/team to disk.
self.cacheAppIDs(team: team, session: session) { (result) in
let result = result.map { _ in (team, certificate, session) }
self.finish(result)
}
}
} }
} }
} }
@@ -173,21 +187,21 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
} }
} }
override func finish(_ result: Result<(ALTSigner, ALTAppleAPISession), Error>) override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>)
{ {
guard !self.isFinished else { return } guard !self.isFinished else { return }
print("Finished authenticating with result:", result) print("Finished authenticating with result:", result.error?.localizedDescription ?? "success")
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.perform { context.perform {
do do
{ {
let (signer, session) = try result.get() let (altTeam, altCertificate, session) = try result.get()
guard guard
let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), signer.team.account.identifier), in: context), let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context),
let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), signer.team.identifier), in: context) let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context)
else { throw AuthenticationError.noTeam } else { throw AuthenticationError.noTeam }
// Account // Account
@@ -214,24 +228,19 @@ class AuthenticationOperation: ResultOperation<(ALTSigner, ALTAppleAPISession)>
team.isActiveTeam = false team.isActiveTeam = false
} }
if let altStoreApp = InstalledApp.fetchAltStore(in: context), altStoreApp.team == nil
{
// No team assigned to AltStore app yet, so assume this team was used to originally install it.
altStoreApp.team = team
}
// Save // Save
try context.save() try context.save()
// Update keychain // Update keychain
Keychain.shared.appleIDEmailAddress = signer.team.account.appleID Keychain.shared.appleIDEmailAddress = altTeam.account.appleID
Keychain.shared.appleIDPassword = self.appleIDPassword Keychain.shared.appleIDPassword = self.appleIDPassword
Keychain.shared.signingCertificate = signer.certificate.p12Data() Keychain.shared.signingCertificate = altCertificate.p12Data()
Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier
self.showInstructionsIfNecessary() { (didShowInstructions) in self.showInstructionsIfNecessary() { (didShowInstructions) in
let signer = ALTSigner(team: altTeam, certificate: altCertificate)
// Refresh screen must go last since a successful refresh will cause the app to quit. // Refresh screen must go last since a successful refresh will cause the app to quit.
self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in
super.finish(result) super.finish(result)
@@ -340,7 +349,7 @@ private extension AuthenticationOperation
func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void) func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void)
{ {
let fetchAnisetteDataOperation = FetchAnisetteDataOperation(group: self.group) let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context)
fetchAnisetteDataOperation.resultHandler = { (result) in fetchAnisetteDataOperation.resultHandler = { (result) in
switch result switch result
{ {
@@ -557,13 +566,38 @@ private extension AuthenticationOperation
} }
} }
func cacheAppIDs(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void) func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{ {
let group = OperationGroup() guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
group.signer = signer return completionHandler(.failure(OperationError.unknownUDID))
group.session = session }
let fetchAppIDsOperation = FetchAppIDsOperation(group: group) ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == udid })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context)
fetchAppIDsOperation.resultHandler = { (result) in fetchAppIDsOperation.resultHandler = { (result) in
do do
{ {
@@ -611,8 +645,7 @@ private extension AuthenticationOperation
#else #else
DispatchQueue.main.async { DispatchQueue.main.async {
let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController
refreshViewController.signer = signer refreshViewController.context = self.context
refreshViewController.session = session
refreshViewController.completionHandler = { _ in refreshViewController.completionHandler = { _ in
completionHandler(true) completionHandler(true)
} }

View File

@@ -16,26 +16,24 @@ import Roxas
@objc(FetchAnisetteDataOperation) @objc(FetchAnisetteDataOperation)
class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData> class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
{ {
let group: OperationGroup let context: OperationContext
init(group: OperationGroup) init(context: OperationContext)
{ {
self.group = group self.context = context
super.init()
} }
override func main() override func main()
{ {
super.main() super.main()
if let error = self.group.error if let error = self.context.error
{ {
self.finish(.failure(error)) self.finish(.failure(error))
return return
} }
guard let server = self.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
ServerManager.shared.connect(to: server) { (result) in ServerManager.shared.connect(to: server) { (result) in
switch result switch result
@@ -55,7 +53,7 @@ class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>
case .success: case .success:
print("Waiting for anisette data...") print("Waiting for anisette data...")
connection.receiveResponse() { (result) in connection.receiveResponse() { (result) in
print("Receiving anisette data:", result) print("Receiving anisette data:", result.error?.localizedDescription ?? "success")
switch result switch result
{ {

View File

@@ -16,13 +16,13 @@ import Roxas
@objc(FetchAppIDsOperation) @objc(FetchAppIDsOperation)
class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)> class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{ {
let group: OperationGroup let context: AuthenticatedOperationContext
let context: NSManagedObjectContext let managedObjectContext: NSManagedObjectContext
init(group: OperationGroup, context: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()) init(context: AuthenticatedOperationContext, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
{ {
self.group = group
self.context = context self.context = context
self.managedObjectContext = managedObjectContext
super.init() super.init()
} }
@@ -31,24 +31,24 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
{ {
super.main() super.main()
if let error = self.group.error if let error = self.context.error
{ {
self.finish(.failure(error)) self.finish(.failure(error))
return return
} }
guard guard
let team = self.group.signer?.team, let team = self.context.team,
let session = self.group.session let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
self.context.perform { self.managedObjectContext.perform {
do do
{ {
let fetchedAppIDs = try Result(appIDs, error).get() let fetchedAppIDs = try Result(appIDs, error).get()
guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.context) else { throw OperationError.notAuthenticated } guard let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), team.identifier), in: self.managedObjectContext) else { throw OperationError.notAuthenticated }
let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier } let fetchedIdentifiers = fetchedAppIDs.map { $0.identifier }
@@ -57,11 +57,11 @@ class FetchAppIDsOperation: ResultOperation<([AppID], NSManagedObjectContext)>
#keyPath(AppID.team), team, #keyPath(AppID.team), team,
#keyPath(AppID.identifier), fetchedIdentifiers) #keyPath(AppID.identifier), fetchedIdentifiers)
let deletedAppIDs = try self.context.fetch(deletedAppIDsRequest) let deletedAppIDs = try self.managedObjectContext.fetch(deletedAppIDsRequest)
deletedAppIDs.forEach { self.context.delete($0) } deletedAppIDs.forEach { self.managedObjectContext.delete($0) }
let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.context) } let appIDs = fetchedAppIDs.map { AppID($0, team: team, context: self.managedObjectContext) }
self.finish(.success((appIDs, self.context))) self.finish(.success((appIDs, self.managedObjectContext)))
} }
catch catch
{ {

View File

@@ -0,0 +1,398 @@
//
// FetchProvisioningProfilesOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(FetchProvisioningProfilesOperation)
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
init(context: AppOperationContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 1
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard
let app = self.context.app,
let team = self.context.team,
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
do
{
self.progress.completedUnitCount += 1
let profile = try result.get()
var profiles = [app.bundleIdentifier: profile]
var error: Error?
let dispatchGroup = DispatchGroup()
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
self.progress.completedUnitCount += 1
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{
self.finish(.failure(error))
}
else
{
self.finish(.success(profiles))
}
}
}
catch
{
self.finish(.failure(error))
}
}
}
func process<T>(_ result: Result<T, Error>) -> T?
{
switch result
{
case .failure(let error):
self.finish(.failure(error))
return nil
case .success(let value):
guard !self.isCancelled else {
self.finish(.failure(OperationError.cancelled))
return nil
}
return value
}
}
}
extension FetchProvisioningProfilesOperation
{
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let preferredBundleID: String
// Check if we have already installed this app with this team before.
let predicate = NSPredicate(format: "%K == %@ AND %K == %@",
#keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier,
#keyPath(InstalledApp.team.identifier), team.identifier)
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
{
#if DEBUG
if app.bundleIdentifier == StoreApp.altstoreAppID || app.bundleIdentifier == StoreApp.alternativeAltStoreAppID
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
}
else
{
preferredBundleID = installedApp.resignedBundleIdentifier
}
#else
// This app is already installed, so use the same resigned bundle identifier as before.
// This way, if we change the identifier format (again), AltStore will continue to use
// the old bundle identifier to prevent it from installing as a new app.
preferredBundleID = installedApp.resignedBundleIdentifier
#endif
}
else
{
// This app isn't already installed, so create the resigned bundle identifier ourselves.
// Or, if the app _is_ installed but with a different team, we need to create a new
// bundle identifier anyway to prevent collisions with the previous team.
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
if app.bundleIdentifier == StoreApp.altstoreAppID || app.bundleIdentifier == StoreApp.alternativeAltStoreAppID
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
}
else
{
preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
}
let preferredName: String
if let parentApp = parentApp
{
preferredName = parentApp.name + " " + app.name
}
else
{
preferredName = app.name
}
// Register
self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
completionHandler(result)
}
}
}
}
}
}
}
}
}
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier })
{
completionHandler(.success(appID))
}
else
{
let requiredAppIDs = 1 + application.appExtensions.count
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
if team.type == .free
{
if requiredAppIDs > availableAppIDs
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
do
{
do
{
let appID = try Result(appID, error).get()
completionHandler(.success(appID))
}
catch ALTAppleAPIError.maximumAppIDLimitReached
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
return (feature, value)
}
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
{
features[.appGroups] = true
}
var updateFeatures = false
// Determine whether the required features are already enabled for the AppID.
for (feature, value) in features
{
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
{
// AppID already has this feature enabled and the values are the same.
continue
}
else
{
// AppID either doesn't have this feature enabled or the value has changed,
// so we need to update it to reflect new values.
updateFeatures = true
break
}
}
if updateFeatures
{
let appID = appID.copy() as! ALTAppID
appID.features = features
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
}
}
else
{
completionHandler(.success(appID))
}
}
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
// TODO: Handle apps belonging to more than one app group.
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
return completionHandler(.success(appID))
}
func finish(_ result: Result<ALTAppGroup, Error>)
{
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let group):
// Assign App Group
// TODO: Determine whether app already belongs to app group.
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
let result = result.map { _ in appID }
completionHandler(result)
}
}
}
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let groups):
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
{
finish(.success(group))
}
else
{
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
finish(Result(group, error))
}
}
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success:
// Fetch new provisiong profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}

View File

@@ -23,27 +23,31 @@ private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificatio
@objc(FindServerOperation) @objc(FindServerOperation)
class FindServerOperation: ResultOperation<Server> class FindServerOperation: ResultOperation<Server>
{ {
let group: OperationGroup let context: OperationContext
private var isWiredServerConnectionAvailable = false private var isWiredServerConnectionAvailable = false
init(group: OperationGroup)
{
self.group = group
super.init()
}
init(context: OperationContext = OperationContext())
{
self.context = context
}
override func main() override func main()
{ {
super.main() super.main()
if let error = self.group.error if let error = self.context.error
{ {
self.finish(.failure(error)) self.finish(.failure(error))
return return
} }
if let server = self.context.server
{
self.finish(.success(server))
return
}
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
// Prepare observers to receive callback from wired server (if connected). // Prepare observers to receive callback from wired server (if connected).

View File

@@ -16,11 +16,11 @@ import Roxas
@objc(InstallAppOperation) @objc(InstallAppOperation)
class InstallAppOperation: ResultOperation<InstalledApp> class InstallAppOperation: ResultOperation<InstalledApp>
{ {
let context: AppOperationContext let context: InstallAppOperationContext
private var didCleanUp = false private var didCleanUp = false
init(context: AppOperationContext) init(context: InstallAppOperationContext)
{ {
self.context = context self.context = context
@@ -40,6 +40,7 @@ class InstallAppOperation: ResultOperation<InstalledApp>
} }
guard guard
let certificate = self.context.certificate,
let resignedApp = self.context.resignedApp, let resignedApp = self.context.resignedApp,
let connection = self.context.installationConnection let connection = self.context.installationConnection
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
@@ -57,10 +58,10 @@ class InstallAppOperation: ResultOperation<InstalledApp>
} }
else else
{ {
installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, context: backgroundContext) installedApp = InstalledApp(resignedApp: resignedApp, originalBundleIdentifier: self.context.bundleIdentifier, certificateSerialNumber: certificate.serialNumber, context: backgroundContext)
} }
installedApp.update(resignedApp: resignedApp) installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber)
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext) if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
{ {
@@ -108,7 +109,7 @@ class InstallAppOperation: ResultOperation<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. // 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() self.cleanUp()
self.context.group.beginInstallationHandler?(installedApp) self.context.beginInstallationHandler?(installedApp)
let request = BeginInstallationRequest() let request = BeginInstallationRequest()
connection.send(request) { (result) in connection.send(request) { (result) in

View File

@@ -0,0 +1,79 @@
//
// Contexts.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Network
import AltSign
class OperationContext
{
var server: Server?
var error: Error?
}
class AuthenticatedOperationContext: OperationContext
{
var session: ALTAppleAPISession?
var team: ALTTeam?
var certificate: ALTCertificate?
weak var authenticationOperation: AuthenticationOperation?
}
@dynamicMemberLookup
class AppOperationContext
{
let bundleIdentifier: String
private let authenticatedContext: AuthenticatedOperationContext
var app: ALTApplication?
var provisioningProfiles: [String: ALTProvisioningProfile]?
var isFinished = false
var error: Error? {
get {
return _error ?? self.authenticatedContext.error
}
set {
_error = newValue
}
}
private var _error: Error?
init(bundleIdentifier: String, authenticatedContext: AuthenticatedOperationContext)
{
self.bundleIdentifier = bundleIdentifier
self.authenticatedContext = authenticatedContext
}
subscript<T>(dynamicMember keyPath: WritableKeyPath<AuthenticatedOperationContext, T>) -> T
{
return self.authenticatedContext[keyPath: keyPath]
}
}
class InstallAppOperationContext: AppOperationContext
{
lazy var temporaryDirectory: URL = {
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) }
catch { self.error = error }
return temporaryDirectory
}()
var resignedApp: ALTApplication?
var installationConnection: ServerConnection?
var beginInstallationHandler: ((InstalledApp) -> Void)?
}

View File

@@ -1,86 +0,0 @@
//
// OperationGroup.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
class OperationGroup
{
let progress = Progress.discreteProgress(totalUnitCount: 0)
var completionHandler: ((Result<[String: Result<InstalledApp, Error>], Error>) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)?
var session: ALTAppleAPISession?
var server: Server?
var signer: ALTSigner?
var error: Error?
var results = [String: Result<InstalledApp, Error>]()
private var progressByBundleIdentifier = [String: Progress]()
private let operationQueue = OperationQueue()
private let installOperationQueue = OperationQueue()
init()
{
// Enforce only one installation at a time.
self.installOperationQueue.maxConcurrentOperationCount = 1
}
func cancel()
{
self.operationQueue.cancelAllOperations()
self.installOperationQueue.cancelAllOperations()
}
func addOperations(_ operations: [Operation])
{
for operation in operations
{
if let installOperation = operation as? InstallAppOperation
{
if let previousOperation = self.installOperationQueue.operations.last
{
// Ensures they execute in the order they're added, since isReady is still false at this point.
installOperation.addDependency(previousOperation)
}
self.installOperationQueue.addOperation(installOperation)
}
else
{
self.operationQueue.addOperation(operation)
}
}
}
func set(_ progress: Progress, for app: AppProtocol)
{
self.progressByBundleIdentifier[app.bundleIdentifier] = progress
self.progress.totalUnitCount += 1
self.progress.addChild(progress, withPendingUnitCount: 1)
}
func progress(for app: AppProtocol) -> Progress?
{
return self.progress(forAppWithBundleIdentifier: app.bundleIdentifier)
}
func progress(forAppWithBundleIdentifier bundleIdentifier: String) -> Progress?
{
let progress = self.progressByBundleIdentifier[bundleIdentifier]
return progress
}
}

View File

@@ -1,81 +0,0 @@
//
// PrepareDeveloperAccountOperation.swift
// AltStore
//
// Created by Riley Testut on 1/7/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
import AltSign
@objc(PrepareDeveloperAccountOperation)
class PrepareDeveloperAccountOperation: ResultOperation<Void>
{
let group: OperationGroup
init(group: OperationGroup)
{
self.group = group
super.init()
self.progress.totalUnitCount = 2
}
override func main()
{
super.main()
if let error = self.group.error
{
self.finish(.failure(error))
return
}
guard
let signer = self.group.signer,
let session = self.group.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
// Register Device
self.registerCurrentDevice(for: signer.team, session: session) { (result) in
let result = result.map { _ in () }
self.finish(result)
}
}
}
private extension PrepareDeveloperAccountOperation
{
func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else {
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
if let device = devices.first(where: { $0.identifier == udid })
{
completionHandler(.success(device))
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
completionHandler(Result(device, error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}

View File

@@ -0,0 +1,125 @@
//
// RefreshAppOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltKit
import Roxas
@objc(RefreshAppOperation)
class RefreshAppOperation: ResultOperation<InstalledApp>
{
let context: AppOperationContext
// Strong reference to managedObjectContext to keep it alive until we're finished.
let managedObjectContext: NSManagedObjectContext
init(context: AppOperationContext)
{
self.context = context
self.managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
super.init()
}
override func main()
{
super.main()
do
{
if let error = self.context.error
{
throw error
}
guard
let server = self.context.server,
let app = self.context.app,
let team = self.context.team,
let profiles = self.context.provisioningProfiles
else { throw OperationError.invalidParameters }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
print("Sending refresh app request...")
var activeProfiles: Set<String>?
if team.type == .free
{
let activeApps = InstalledApp.all(in: context)
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
}
let request = InstallProvisioningProfilesRequest(udid: udid, provisioningProfiles: Set(profiles.values), activeProfiles: activeProfiles)
connection.send(request) { (result) in
print("Sent refresh app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for refresh app response...")
connection.receiveResponse() { (result) in
print("Receiving refresh app response:", result)
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(.error(let response)): self.finish(.failure(response.error))
case .success(.installProvisioningProfiles):
self.managedObjectContext.perform {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
return self.finish(.failure(OperationError.invalidApp))
}
self.progress.completedUnitCount += 1
if let provisioningProfile = profiles[app.bundleIdentifier]
{
installedApp.update(provisioningProfile: provisioningProfile)
}
for installedExtension in installedApp.appExtensions
{
guard let provisioningProfile = profiles[installedExtension.bundleIdentifier] else { continue }
installedExtension.update(provisioningProfile: provisioningProfile)
}
self.finish(.success(installedApp))
}
case .success: self.finish(.failure(ALTServerError(.unknownRequest)))
}
}
}
}
}
}
}
}
catch
{
self.finish(.failure(error))
}
}
}

View File

@@ -0,0 +1,77 @@
//
// RefreshGroup.swift
// AltStore
//
// Created by Riley Testut on 6/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltSign
class RefreshGroup: NSObject
{
let context: AuthenticatedOperationContext
let progress = Progress.discreteProgress(totalUnitCount: 0)
var completionHandler: (([String: Result<InstalledApp, Error>]) -> Void)?
var beginInstallationHandler: ((InstalledApp) -> Void)?
private(set) var results = [String: Result<InstalledApp, Error>]()
private var isFinished = false
private let dispatchGroup = DispatchGroup()
private var operations: [Foundation.Operation] = []
init(context: AuthenticatedOperationContext = AuthenticatedOperationContext())
{
self.context = context
super.init()
}
func add(_ operations: [Foundation.Operation])
{
for operation in operations
{
self.dispatchGroup.enter()
operation.completionBlock = { [weak self] in
self?.dispatchGroup.leave()
}
}
if self.operations.isEmpty && !operations.isEmpty
{
self.dispatchGroup.notify(queue: .global()) {
self.finish()
}
}
self.operations.append(contentsOf: operations)
}
func set(_ result: Result<InstalledApp, Error>, forAppWithBundleIdentifier bundleIdentifier: String)
{
self.results[bundleIdentifier] = result
}
func cancel()
{
self.operations.forEach { $0.cancel() }
}
}
private extension RefreshGroup
{
func finish()
{
guard !self.isFinished else { return }
self.isFinished = true
self.completionHandler?(self.results)
}
}

View File

@@ -14,9 +14,9 @@ import AltSign
@objc(ResignAppOperation) @objc(ResignAppOperation)
class ResignAppOperation: ResultOperation<ALTApplication> class ResignAppOperation: ResultOperation<ALTApplication>
{ {
let context: AppOperationContext let context: InstallAppOperationContext
init(context: AppOperationContext) init(context: InstallAppOperationContext)
{ {
self.context = context self.context = context
@@ -37,46 +37,42 @@ class ResignAppOperation: ResultOperation<ALTApplication>
guard guard
let app = self.context.app, let app = self.context.app,
let signer = self.context.group.signer, let profiles = self.context.provisioningProfiles,
let session = self.context.group.session let team = self.context.team,
let certificate = self.context.certificate
else { return self.finish(.failure(OperationError.invalidParameters)) } else { return self.finish(.failure(OperationError.invalidParameters)) }
// Prepare Provisioning Profiles // Prepare app bundle
self.prepareProvisioningProfiles(app.fileURL, team: signer.team, session: session) { (result) in let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
guard let profiles = self.process(result) else { return } self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in
guard let appBundleURL = self.process(result) else { return }
// Prepare app bundle print("Resigning App:", self.context.bundleIdentifier)
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles) { (result) in // Resign app bundle
guard let appBundleURL = self.process(result) else { return } let resignProgress = self.resignAppBundle(at: appBundleURL, team: team, certificate: certificate, profiles: Array(profiles.values)) { (result) in
guard let resignedURL = self.process(result) else { return }
print("Resigning App:", self.context.bundleIdentifier) // Finish
do
// Resign app bundle {
let resignProgress = self.resignAppBundle(at: appBundleURL, signer: signer, profiles: Array(profiles.values)) { (result) in let destinationURL = InstalledApp.refreshedIPAURL(for: app)
guard let resignedURL = self.process(result) else { return } try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
// Finish // Use appBundleURL since we need an app bundle, not .ipa.
do guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
{ self.finish(.success(resignedApplication))
let destinationURL = InstalledApp.refreshedIPAURL(for: app) }
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true) catch
{
// Use appBundleURL since we need an app bundle, not .ipa. self.finish(.failure(error))
guard let resignedApplication = ALTApplication(fileURL: appBundleURL) else { throw OperationError.invalidApp }
self.finish(.success(resignedApplication))
}
catch
{
self.finish(.failure(error))
}
} }
prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
} }
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1) prepareAppProgress.addChild(resignProgress, withPendingUnitCount: 1)
} }
prepareAppProgress.addChild(prepareAppBundleProgress, withPendingUnitCount: 1)
} }
func process<T>(_ result: Result<T, Error>) -> T? func process<T>(_ result: Result<T, Error>) -> T?
@@ -100,310 +96,6 @@ class ResignAppOperation: ResultOperation<ALTApplication>
private extension ResignAppOperation private extension ResignAppOperation
{ {
func prepareProvisioningProfiles(_ fileURL: URL, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
{
guard let app = ALTApplication(fileURL: fileURL) else { return completionHandler(.failure(OperationError.invalidApp)) }
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
var profiles = [app.bundleIdentifier: profile]
var error: Error?
let dispatchGroup = DispatchGroup()
for appExtension in app.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: app, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(profiles))
}
}
}
}
}
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let preferredBundleID: String
// Check if we have already installed this app with this team before.
let predicate = NSPredicate(format: "%K == %@ AND %K == %@",
#keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier,
#keyPath(InstalledApp.team.identifier), team.identifier)
if let installedApp = InstalledApp.first(satisfying: predicate, in: context)
{
// This app is already installed, so use the same resigned bundle identifier as before.
// This way, if we change the identifier format (again), AltStore will continue to use
// the old bundle identifier to prevent it from installing as a new app.
preferredBundleID = installedApp.resignedBundleIdentifier
}
else
{
// This app isn't already installed, so create the resigned bundle identifier ourselves.
// Or, if the app _is_ installed but with a different team, we need to create a new
// bundle identifier anyway to prevent collisions with the previous team.
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
preferredBundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
let preferredName: String
if let parentApp = parentApp
{
preferredName = "\(parentApp.name) - \(app.name)"
}
else
{
preferredName = app.name
}
// Register
self.registerAppID(for: app, name: preferredName, bundleIdentifier: preferredBundleID, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
completionHandler(result)
}
}
}
}
}
}
}
}
}
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier })
{
completionHandler(.success(appID))
}
else
{
let requiredAppIDs = 1 + application.appExtensions.count
let availableAppIDs = max(0, Team.maximumFreeAppIDs - appIDs.count)
let sortedExpirationDates = appIDs.compactMap { $0.expirationDate }.sorted(by: { $0 < $1 })
if team.type == .free
{
if requiredAppIDs > availableAppIDs
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
ALTAppleAPI.shared.addAppID(withName: name, bundleIdentifier: bundleIdentifier, team: team, session: session) { (appID, error) in
do
{
do
{
let appID = try Result(appID, error).get()
completionHandler(.success(appID))
}
catch ALTAppleAPIError.maximumAppIDLimitReached
{
if let expirationDate = sortedExpirationDates.first
{
throw OperationError.maximumAppIDLimitReached(application: application, requiredAppIDs: requiredAppIDs, availableAppIDs: availableAppIDs, nextExpirationDate: expirationDate)
}
else
{
throw ALTAppleAPIError(.maximumAppIDLimitReached)
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
return (feature, value)
}
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
{
features[.appGroups] = true
}
var updateFeatures = false
// Determine whether the required features are already enabled for the AppID.
for (feature, value) in features
{
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
{
// AppID already has this feature enabled and the values are the same.
continue
}
else
{
// AppID either doesn't have this feature enabled or the value has changed,
// so we need to update it to reflect new values.
updateFeatures = true
break
}
}
if updateFeatures
{
let appID = appID.copy() as! ALTAppID
appID.features = features
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
}
}
else
{
completionHandler(.success(appID))
}
}
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
// TODO: Handle apps belonging to more than one app group.
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
return completionHandler(.success(appID))
}
func finish(_ result: Result<ALTAppGroup, Error>)
{
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let group):
// Assign App Group
// TODO: Determine whether app already belongs to app group.
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
let result = result.map { _ in appID }
completionHandler(result)
}
}
}
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let groups):
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
{
finish(.success(group))
}
else
{
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
finish(Result(group, error))
}
}
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success:
// Fetch new provisiong profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{ {
let progress = Progress.discreteProgress(totalUnitCount: 1) let progress = Progress.discreteProgress(totalUnitCount: 1)
@@ -511,8 +203,9 @@ private extension ResignAppOperation
return progress return progress
} }
func resignAppBundle(at fileURL: URL, signer: ALTSigner, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress func resignAppBundle(at fileURL: URL, team: ALTTeam, certificate: ALTCertificate, profiles: [ALTProvisioningProfile], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{ {
let signer = ALTSigner(team: team, certificate: certificate)
let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in let progress = signer.signApp(at: fileURL, provisioningProfiles: profiles) { (success, error) in
do do
{ {

View File

@@ -39,7 +39,7 @@ class SendAppOperation: ResultOperation<ServerConnection>
return return
} }
guard let app = self.context.app, let server = self.context.group.server else { return self.finish(.failure(OperationError.invalidParameters)) } guard let app = self.context.app, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa. // self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
let fileURL = InstalledApp.refreshedIPAURL(for: app) let fileURL = InstalledApp.refreshedIPAURL(for: app)