@capgo/capacitor-updater 8.51.14 → 8.51.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoCapacitorUpdater.podspec +0 -2
- package/Package.swift +4 -5
- package/README.md +1 -1
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +55 -22
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +59 -29
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +11 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +57 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +4 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +4 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +114 -30
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoRawRsa.swift +36 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +56 -20
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +73 -251
- package/package.json +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -39
|
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
96
|
deinit {
|
|
97
97
|
implementation.shutdown()
|
|
98
98
|
}
|
|
99
|
-
private let pluginVersion: String = "8.51.
|
|
99
|
+
private let pluginVersion: String = "8.51.16"
|
|
100
100
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
101
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
102
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -191,9 +191,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
191
191
|
|
|
192
192
|
// Lock to ensure cleanup completes before downloads start
|
|
193
193
|
private let cleanupLock = NSLock()
|
|
194
|
+
// Short lock for cleanupComplete/cleanupTimedOut only. Never hold across cleanupGroup.wait.
|
|
195
|
+
private let cleanupStateLock = NSLock()
|
|
194
196
|
private let defaultChannelStateLock = NSLock()
|
|
195
197
|
private let cleanupGroup = DispatchGroup()
|
|
196
198
|
private var cleanupComplete = false
|
|
199
|
+
private var cleanupTimedOut = false
|
|
197
200
|
private var cleanupThread: Thread?
|
|
198
201
|
private var defaultChannelCleanupMustRetry = false
|
|
199
202
|
private var persistCustomId = false
|
|
@@ -216,6 +219,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
216
219
|
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
217
220
|
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
218
221
|
private var pendingNotifyAppReady = false
|
|
222
|
+
private let semaphoreWaitTestingLock = NSLock()
|
|
223
|
+
private var didEnterSemaphoreWaitForTesting = false
|
|
219
224
|
|
|
220
225
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
221
226
|
|
|
@@ -427,7 +432,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
427
432
|
|
|
428
433
|
// Downloads (including shake-menu / CapgoUpdater entry points) wait on this gate.
|
|
429
434
|
self.implementation.beforeDownload = { [weak self] in
|
|
430
|
-
self?.waitForCleanupIfNeeded()
|
|
435
|
+
try self?.waitForCleanupIfNeeded()
|
|
431
436
|
}
|
|
432
437
|
// Always run async cleanup: delete obsolete bundles on native update (when enabled)
|
|
433
438
|
// and sweep orphan directories every launch. Must not block app startup.
|
|
@@ -594,6 +599,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
594
599
|
|
|
595
600
|
private func semaphoreWait(waitTime: Int) {
|
|
596
601
|
// print("\\(CapgoUpdater.TAG) semaphoreWait \\(waitTime)")
|
|
602
|
+
semaphoreWaitTestingLock.lock()
|
|
603
|
+
didEnterSemaphoreWaitForTesting = true
|
|
604
|
+
semaphoreWaitTestingLock.unlock()
|
|
597
605
|
let result = semaphoreReady.wait(timeout: .now() + .milliseconds(waitTime))
|
|
598
606
|
if result == .timedOut {
|
|
599
607
|
logger.error("Semaphore wait timed out after \(waitTime)ms")
|
|
@@ -1035,9 +1043,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1035
1043
|
}
|
|
1036
1044
|
|
|
1037
1045
|
private func cleanupObsoleteVersions(resetWhenUpdate: Bool = true, didResetCurrentBundle: Bool = false) {
|
|
1038
|
-
// Enter before
|
|
1046
|
+
// Enter before publishing incomplete state so waiters cannot hit an empty group.
|
|
1047
|
+
self.cleanupStateLock.lock()
|
|
1039
1048
|
self.cleanupComplete = false
|
|
1049
|
+
self.cleanupTimedOut = false
|
|
1040
1050
|
self.cleanupGroup.enter()
|
|
1051
|
+
self.cleanupStateLock.unlock()
|
|
1041
1052
|
cleanupThread = Thread {
|
|
1042
1053
|
let bgTaskLock = NSLock()
|
|
1043
1054
|
var cleanupBackgroundTask = UIBackgroundTaskIdentifier.invalid
|
|
@@ -1054,7 +1065,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1054
1065
|
}
|
|
1055
1066
|
self.cleanupLock.lock()
|
|
1056
1067
|
defer {
|
|
1068
|
+
self.cleanupStateLock.lock()
|
|
1057
1069
|
self.cleanupComplete = true
|
|
1070
|
+
self.cleanupStateLock.unlock()
|
|
1058
1071
|
self.cleanupLock.unlock()
|
|
1059
1072
|
self.cleanupGroup.leave()
|
|
1060
1073
|
endCleanupBackgroundTask()
|
|
@@ -1139,17 +1152,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1139
1152
|
cleanupThread?.start()
|
|
1140
1153
|
}
|
|
1141
1154
|
|
|
1142
|
-
private func
|
|
1143
|
-
|
|
1155
|
+
private func cleanupTimeoutError() -> NSError {
|
|
1156
|
+
NSError(
|
|
1157
|
+
domain: "CapacitorUpdater",
|
|
1158
|
+
code: 1,
|
|
1159
|
+
userInfo: [NSLocalizedDescriptionKey: "Cleanup did not finish before download"]
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private func waitForCleanupIfNeeded() throws {
|
|
1164
|
+
cleanupStateLock.lock()
|
|
1165
|
+
let alreadyDone = cleanupComplete
|
|
1166
|
+
let alreadyTimedOut = cleanupTimedOut
|
|
1167
|
+
cleanupStateLock.unlock()
|
|
1168
|
+
if alreadyDone {
|
|
1144
1169
|
return // Already done, no need to wait
|
|
1145
1170
|
}
|
|
1171
|
+
if alreadyTimedOut {
|
|
1172
|
+
throw cleanupTimeoutError()
|
|
1173
|
+
}
|
|
1146
1174
|
|
|
1147
1175
|
logger.info("Waiting for cleanup to complete before starting download...")
|
|
1148
1176
|
let result = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1149
1177
|
if result == .timedOut {
|
|
1150
1178
|
logger.warn("Cleanup wait timed out after 60s, cancelling leftover cleanup")
|
|
1151
1179
|
cleanupThread?.cancel()
|
|
1152
|
-
|
|
1180
|
+
let cancelled = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1181
|
+
if cancelled == .timedOut {
|
|
1182
|
+
cleanupStateLock.lock()
|
|
1183
|
+
cleanupTimedOut = true
|
|
1184
|
+
cleanupStateLock.unlock()
|
|
1185
|
+
logger.error("Cleanup did not finish after cancel, aborting download")
|
|
1186
|
+
throw cleanupTimeoutError()
|
|
1187
|
+
}
|
|
1153
1188
|
return
|
|
1154
1189
|
}
|
|
1155
1190
|
logger.info("Cleanup finished, proceeding with download")
|
|
@@ -1568,8 +1603,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1568
1603
|
}
|
|
1569
1604
|
|
|
1570
1605
|
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
1571
|
-
// Manual/preview downloads must wait too - launch orphan sweep can delete their temps.
|
|
1572
|
-
self.waitForCleanupIfNeeded()
|
|
1573
1606
|
guard let url = URL(string: urlString) else {
|
|
1574
1607
|
throw makePreviewError("Invalid download URL")
|
|
1575
1608
|
}
|
|
@@ -1809,6 +1842,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1809
1842
|
}
|
|
1810
1843
|
}
|
|
1811
1844
|
|
|
1845
|
+
private func queueBundleForNextBackgroundInstall(_ next: BundleInfo) -> Bool {
|
|
1846
|
+
guard self.implementation.setNextBundle(next: next.getId()) else {
|
|
1847
|
+
self.logger.error("Failed to queue downloaded bundle as next: \(next.toString())")
|
|
1848
|
+
return false
|
|
1849
|
+
}
|
|
1850
|
+
return true
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1812
1853
|
private func applyDownloadedBundleForDirectUpdate(_ next: BundleInfo) -> Bool {
|
|
1813
1854
|
let previousState = self.implementation.captureResetState()
|
|
1814
1855
|
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
@@ -3993,6 +4034,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3993
4034
|
isAutoUpdateModeEnabled(autoUpdateMode) && autoUpdateMode != autoUpdateModeOnlyDownload
|
|
3994
4035
|
}
|
|
3995
4036
|
|
|
4037
|
+
/// Incomplete leftovers must be downloaded again. Android already skips
|
|
4038
|
+
/// `DOWNLOADING` here; iOS used to treat them as ready, then `setNextBundle`
|
|
4039
|
+
/// failed quietly and the update never applied.
|
|
4040
|
+
static func shouldRetryDownloadForExistingBundle(_ bundle: BundleInfo) -> Bool {
|
|
4041
|
+
bundle.isDeleted() || bundle.isDeleting() || bundle.isDownloading()
|
|
4042
|
+
}
|
|
4043
|
+
|
|
3996
4044
|
static func isDirectUpdateMode(_ directUpdateMode: String) -> Bool {
|
|
3997
4045
|
directUpdateMode == autoUpdateModeInstall || directUpdateMode == autoUpdateModeLaunch || directUpdateMode == autoUpdateModeAlways
|
|
3998
4046
|
}
|
|
@@ -4063,6 +4111,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4063
4111
|
pendingNotifyAppReady = true
|
|
4064
4112
|
}
|
|
4065
4113
|
|
|
4114
|
+
func resetSemaphoreWaitTestingStateForTesting() {
|
|
4115
|
+
semaphoreWaitTestingLock.lock()
|
|
4116
|
+
didEnterSemaphoreWaitForTesting = false
|
|
4117
|
+
semaphoreWaitTestingLock.unlock()
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
var didEnterSemaphoreWaitForTestingState: Bool {
|
|
4121
|
+
semaphoreWaitTestingLock.lock()
|
|
4122
|
+
defer { semaphoreWaitTestingLock.unlock() }
|
|
4123
|
+
return didEnterSemaphoreWaitForTesting
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4066
4126
|
func clearPendingNotifyAppReadyForTesting() {
|
|
4067
4127
|
clearPendingNotifyAppReady()
|
|
4068
4128
|
}
|
|
@@ -4220,7 +4280,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4220
4280
|
func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
|
|
4221
4281
|
// Live update checks/downloads are user-visible work. Using `.background`
|
|
4222
4282
|
// lets the scheduler starve them for minutes while the app is active.
|
|
4223
|
-
DispatchQueue.global(qos: .
|
|
4283
|
+
DispatchQueue.global(qos: .userInitiated).async(execute: work)
|
|
4224
4284
|
}
|
|
4225
4285
|
|
|
4226
4286
|
private func beginDownloadBackgroundTask() {
|
|
@@ -4271,8 +4331,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4271
4331
|
}
|
|
4272
4332
|
|
|
4273
4333
|
self.runBackgroundDownloadWork {
|
|
4274
|
-
// Wait for cleanup to complete before starting download
|
|
4275
|
-
self.waitForCleanupIfNeeded()
|
|
4276
4334
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
4277
4335
|
self.clearDownloadInProgressState()
|
|
4278
4336
|
return
|
|
@@ -4280,7 +4338,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4280
4338
|
self.beginDownloadBackgroundTask()
|
|
4281
4339
|
self.logger.info("Check for update via \(self.updateUrl)")
|
|
4282
4340
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
4283
|
-
|
|
4341
|
+
var current = self.implementation.getCurrentBundle()
|
|
4284
4342
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
4285
4343
|
self.clearDownloadInProgressState()
|
|
4286
4344
|
self.endBackGroundTask()
|
|
@@ -4299,6 +4357,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4299
4357
|
)
|
|
4300
4358
|
return
|
|
4301
4359
|
}
|
|
4360
|
+
// File mutations wait here. getLatest already ran in parallel with cleanup.
|
|
4361
|
+
do {
|
|
4362
|
+
try self.waitForCleanupIfNeeded()
|
|
4363
|
+
} catch {
|
|
4364
|
+
self.logger.error("Cleanup still running, skipping download")
|
|
4365
|
+
self.clearDownloadInProgressState()
|
|
4366
|
+
self.endBackGroundTask()
|
|
4367
|
+
return
|
|
4368
|
+
}
|
|
4369
|
+
current = self.implementation.getCurrentBundle()
|
|
4302
4370
|
if res.version == "builtin" {
|
|
4303
4371
|
self.logger.info("Latest version is builtin")
|
|
4304
4372
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
@@ -4360,14 +4428,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4360
4428
|
do {
|
|
4361
4429
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
4362
4430
|
var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4431
|
+
let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
|
|
4432
|
+
if needsDownload {
|
|
4433
|
+
if let existing = nextImpl {
|
|
4434
|
+
self.logger.info("Latest bundle already exists in incomplete state (\(existing.getStatus())) and will be deleted, download will overwrite it.")
|
|
4435
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
4436
|
+
let deleted = self.implementation.delete(id: existing.getId(), removeInfo: true)
|
|
4437
|
+
if deleted {
|
|
4438
|
+
self.logger.info("Incomplete bundle deleted: \(existing.toString())")
|
|
4369
4439
|
} else {
|
|
4370
|
-
self.logger.error("Failed to delete
|
|
4440
|
+
self.logger.error("Failed to delete incomplete bundle: \(existing.toString())")
|
|
4371
4441
|
}
|
|
4372
4442
|
}
|
|
4373
4443
|
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
@@ -4454,8 +4524,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4454
4524
|
error: false,
|
|
4455
4525
|
plannedDirectUpdate: plannedDirectUpdate
|
|
4456
4526
|
)
|
|
4457
|
-
} else {
|
|
4458
|
-
_ = self.implementation.setNextBundle(next: next.getId())
|
|
4527
|
+
} else if self.queueBundleForNextBackgroundInstall(next) {
|
|
4459
4528
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4460
4529
|
self.endBackGroundTaskWithNotif(
|
|
4461
4530
|
msg: "Direct update reload failed, update will install next background",
|
|
@@ -4464,20 +4533,35 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4464
4533
|
error: false,
|
|
4465
4534
|
plannedDirectUpdate: plannedDirectUpdate
|
|
4466
4535
|
)
|
|
4536
|
+
} else {
|
|
4537
|
+
self.endBackGroundTaskWithNotif(
|
|
4538
|
+
msg: "Direct update reload failed, and next bundle could not be queued",
|
|
4539
|
+
latestVersionName: latestVersionName,
|
|
4540
|
+
current: current,
|
|
4541
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4542
|
+
)
|
|
4467
4543
|
}
|
|
4468
4544
|
} else if self.shouldAutoSetNextBundle() {
|
|
4469
4545
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
4470
4546
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
4471
4547
|
}
|
|
4472
|
-
self.
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4548
|
+
if self.queueBundleForNextBackgroundInstall(next) {
|
|
4549
|
+
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4550
|
+
self.endBackGroundTaskWithNotif(
|
|
4551
|
+
msg: "update downloaded, will install next background",
|
|
4552
|
+
latestVersionName: latestVersionName,
|
|
4553
|
+
current: current,
|
|
4554
|
+
error: false,
|
|
4555
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4556
|
+
)
|
|
4557
|
+
} else {
|
|
4558
|
+
self.endBackGroundTaskWithNotif(
|
|
4559
|
+
msg: "Update downloaded, but next bundle could not be queued",
|
|
4560
|
+
latestVersionName: latestVersionName,
|
|
4561
|
+
current: current,
|
|
4562
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4563
|
+
)
|
|
4564
|
+
}
|
|
4481
4565
|
} else {
|
|
4482
4566
|
self.logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle")
|
|
4483
4567
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()], retainUntilConsumed: true)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Foundation
|
|
8
|
+
import Security
|
|
9
|
+
import os.log
|
|
10
|
+
|
|
11
|
+
/// Isolated raw RSA primitives for Capgo v2 checksum/session-key recovery.
|
|
12
|
+
/// SonarCloud automatic analysis has no Swift // NOSONAR; swift:S5542 is ignored
|
|
13
|
+
/// via `.sonarcloud.properties` multicriteria because `.rsaEncryptionRaw` is
|
|
14
|
+
/// intentional wire compatibility with Node `privateEncrypt` / `publicDecrypt`.
|
|
15
|
+
enum CapgoRawRsa {
|
|
16
|
+
private static let log = OSLog(subsystem: "ee.forgr.capacitor_updater", category: "RSA")
|
|
17
|
+
|
|
18
|
+
static func supportsCapgoRecovery(key: SecKey) -> Bool {
|
|
19
|
+
SecKeyGetBlockSize(key) == 256
|
|
20
|
+
&& SecKeyIsAlgorithmSupported(key, .encrypt, .rsaEncryptionRaw) // NOSONAR intentional raw RSA for Node privateEncrypt / Capgo checksum recovery
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func recoverSignatureBlock(key: SecKey, data: Data) -> Data? {
|
|
24
|
+
var error: Unmanaged<CFError>?
|
|
25
|
+
guard let recovered = SecKeyCreateEncryptedData(key, .rsaEncryptionRaw, data as CFData, &error) as Data? else { // NOSONAR intentional raw RSA for Node privateEncrypt / Capgo checksum recovery
|
|
26
|
+
os_log(
|
|
27
|
+
"RSA raw recovery failed: %{public}@",
|
|
28
|
+
log: log,
|
|
29
|
+
type: .error,
|
|
30
|
+
String(describing: error?.takeRetainedValue())
|
|
31
|
+
)
|
|
32
|
+
return nil
|
|
33
|
+
}
|
|
34
|
+
return recovered
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -45,7 +45,7 @@ import UIKit
|
|
|
45
45
|
public var timeout: Double = 20
|
|
46
46
|
public var statsUrl: String = ""
|
|
47
47
|
/// Optional gate run before any download touches disk (e.g. wait for launch cleanup).
|
|
48
|
-
public var beforeDownload: (() -> Void)?
|
|
48
|
+
public var beforeDownload: (() throws -> Void)?
|
|
49
49
|
public var channelUrl: String = ""
|
|
50
50
|
public var defaultChannel: String = ""
|
|
51
51
|
public var appId: String = ""
|
|
@@ -781,7 +781,7 @@ import UIKit
|
|
|
781
781
|
}
|
|
782
782
|
}
|
|
783
783
|
|
|
784
|
-
private func extractZipEntry(_ archive: Archive, entry: Entry, to destPath: URL) throws {
|
|
784
|
+
private func extractZipEntry(_ archive: Archive, entry: Entry, to destPath: URL, bufferSize: Int = CryptoCipher.ioBufferBytes()) throws {
|
|
785
785
|
let fileManager = FileManager.default
|
|
786
786
|
|
|
787
787
|
switch entry.type {
|
|
@@ -804,14 +804,14 @@ import UIKit
|
|
|
804
804
|
fileHandle.closeFile()
|
|
805
805
|
}
|
|
806
806
|
|
|
807
|
-
_ = try archive.extract(entry, bufferSize:
|
|
807
|
+
_ = try archive.extract(entry, bufferSize: bufferSize, skipCRC32: true) { data in
|
|
808
808
|
if !data.isEmpty {
|
|
809
809
|
fileHandle.write(data)
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
812
|
case .symlink:
|
|
813
813
|
var linkData = Data()
|
|
814
|
-
_ = try archive.extract(entry, bufferSize:
|
|
814
|
+
_ = try archive.extract(entry, bufferSize: bufferSize, skipCRC32: true) { data in
|
|
815
815
|
linkData.append(data)
|
|
816
816
|
}
|
|
817
817
|
|
|
@@ -840,7 +840,7 @@ import UIKit
|
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
-
|
|
843
|
+
func saveDownloaded(sourceZip: URL, id: String, base: URL, notify: Bool, bufferSize: Int = CryptoCipher.ioBufferBytes()) throws {
|
|
844
844
|
try prepareFolder(source: base)
|
|
845
845
|
let destPersist: URL = base.appendingPathComponent(id)
|
|
846
846
|
let destUnZip: URL = libraryDir.appendingPathComponent(TEMP_UNZIP_PREFIX + randomString(length: 10))
|
|
@@ -887,7 +887,7 @@ import UIKit
|
|
|
887
887
|
try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil)
|
|
888
888
|
}
|
|
889
889
|
|
|
890
|
-
try self.extractZipEntry(archive, entry: entry, to: destPath)
|
|
890
|
+
try self.extractZipEntry(archive, entry: entry, to: destPath, bufferSize: bufferSize)
|
|
891
891
|
|
|
892
892
|
// Update progress
|
|
893
893
|
processedEntries += 1
|
|
@@ -928,10 +928,7 @@ import UIKit
|
|
|
928
928
|
|
|
929
929
|
struct ManifestLookupEntry {
|
|
930
930
|
let hash: String
|
|
931
|
-
/// The manifest's own file name, `.br` suffix included when present.
|
|
932
|
-
/// built-in bundle stores files under this exact name (see
|
|
933
|
-
/// isManifestEntryAvailableLocally), unlike the extracted/cached copy which
|
|
934
|
-
/// is always named without the suffix.
|
|
931
|
+
/// The manifest's own file name, `.br` suffix included when present.
|
|
935
932
|
let originalFileName: String
|
|
936
933
|
}
|
|
937
934
|
|
|
@@ -994,10 +991,14 @@ import UIKit
|
|
|
994
991
|
|
|
995
992
|
// Builtin is already a permanent reuse source (see isManifestEntryAvailableLocally),
|
|
996
993
|
// so there's no need to also duplicate this file into the delta cache
|
|
997
|
-
let
|
|
998
|
-
let
|
|
999
|
-
let
|
|
1000
|
-
|
|
994
|
+
let builtinLookupName = knownEntry?.originalFileName ?? relativePath
|
|
995
|
+
let isBuiltinOrigin: Bool
|
|
996
|
+
if let builtinFilePath = try? Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: builtinLookupName) {
|
|
997
|
+
isBuiltinOrigin = fileManager.fileExists(atPath: builtinFilePath.path) &&
|
|
998
|
+
verifyChecksum(file: builtinFilePath, expectedHash: checksum)
|
|
999
|
+
} else {
|
|
1000
|
+
isBuiltinOrigin = false
|
|
1001
|
+
}
|
|
1001
1002
|
if isBuiltinOrigin {
|
|
1002
1003
|
continue
|
|
1003
1004
|
}
|
|
@@ -1231,7 +1232,10 @@ import UIKit
|
|
|
1231
1232
|
}
|
|
1232
1233
|
|
|
1233
1234
|
let builtinFolder = self.builtinFolderURL()
|
|
1234
|
-
|
|
1235
|
+
// The .br suffix describes transport; builtin assets are uncompressed.
|
|
1236
|
+
guard let builtinFilePath = try? Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: fileName) else {
|
|
1237
|
+
return false
|
|
1238
|
+
}
|
|
1235
1239
|
if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
|
|
1236
1240
|
return true
|
|
1237
1241
|
}
|
|
@@ -1365,12 +1369,12 @@ import UIKit
|
|
|
1365
1369
|
return json
|
|
1366
1370
|
}
|
|
1367
1371
|
|
|
1368
|
-
private func runBeforeDownload() {
|
|
1369
|
-
beforeDownload?()
|
|
1372
|
+
private func runBeforeDownload() throws {
|
|
1373
|
+
try beforeDownload?()
|
|
1370
1374
|
}
|
|
1371
1375
|
|
|
1372
1376
|
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1373
|
-
self.runBeforeDownload()
|
|
1377
|
+
try self.runBeforeDownload()
|
|
1374
1378
|
let id = self.randomString(length: 10)
|
|
1375
1379
|
logger.info("downloadManifest start \(id)")
|
|
1376
1380
|
let destFolder = self.getBundleDirectory(id: id)
|
|
@@ -1474,7 +1478,7 @@ import UIKit
|
|
|
1474
1478
|
let builtinFilePath: URL
|
|
1475
1479
|
do {
|
|
1476
1480
|
destFilePath = try Self.resolveManifestTargetPath(baseDirectory: destFolder, fileName: fileName)
|
|
1477
|
-
builtinFilePath = try Self.
|
|
1481
|
+
builtinFilePath = try Self.resolveManifestTargetPath(baseDirectory: builtinFolder, fileName: fileName)
|
|
1478
1482
|
} catch {
|
|
1479
1483
|
logger.error("Invalid manifest file path: \(fileName)")
|
|
1480
1484
|
self.sendStats(action: "manifest_path_fail", versionName: "\(version):\(fileName)")
|
|
@@ -1736,6 +1740,38 @@ import UIKit
|
|
|
1736
1740
|
try fileManager.copyItem(at: source, to: destination)
|
|
1737
1741
|
}
|
|
1738
1742
|
|
|
1743
|
+
func copyMatchingBuiltinFilesForTests(files: [(source: URL, dest: URL, hash: String)]) throws {
|
|
1744
|
+
let queue = OperationQueue()
|
|
1745
|
+
queue.maxConcurrentOperationCount = CapgoUpdater.manifestMaxConcurrentFiles
|
|
1746
|
+
let lock = NSLock()
|
|
1747
|
+
var firstError: Error?
|
|
1748
|
+
for file in files {
|
|
1749
|
+
queue.addOperation { [weak self] in
|
|
1750
|
+
guard let self else { return }
|
|
1751
|
+
do {
|
|
1752
|
+
guard self.verifyChecksum(file: file.source, expectedHash: file.hash) else {
|
|
1753
|
+
throw NSError(
|
|
1754
|
+
domain: "CapgoInstallPerf",
|
|
1755
|
+
code: 1,
|
|
1756
|
+
userInfo: [NSLocalizedDescriptionKey: "builtin checksum mismatch"]
|
|
1757
|
+
)
|
|
1758
|
+
}
|
|
1759
|
+
try self.copyItemReplacing(from: file.source, to: file.dest)
|
|
1760
|
+
} catch {
|
|
1761
|
+
lock.lock()
|
|
1762
|
+
if firstError == nil {
|
|
1763
|
+
firstError = error
|
|
1764
|
+
}
|
|
1765
|
+
lock.unlock()
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
queue.waitUntilAllOperationsAreFinished()
|
|
1770
|
+
if let firstError {
|
|
1771
|
+
throw firstError
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1739
1775
|
/// Copy via a unique temp name then rename, so a crash cannot leave a
|
|
1740
1776
|
/// non-empty partial file that `isReusableCacheFile` would trust.
|
|
1741
1777
|
private func copyItemAtomically(from source: URL, to destination: URL) throws {
|
|
@@ -1941,7 +1977,7 @@ import UIKit
|
|
|
1941
1977
|
}
|
|
1942
1978
|
|
|
1943
1979
|
public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1944
|
-
self.runBeforeDownload()
|
|
1980
|
+
try self.runBeforeDownload()
|
|
1945
1981
|
let id: String = self.randomString(length: 10)
|
|
1946
1982
|
// Each download uses its own temp files keyed by bundle ID to prevent collisions
|
|
1947
1983
|
if version != getLocalUpdateVersion(for: id) {
|