@capgo/capacitor-updater 8.51.13 → 8.51.15
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/Package.swift +2 -1
- package/README.md +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +62 -29
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +89 -35
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +8 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +65 -39
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +3 -0
- 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/AES.swift +5 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +32 -11
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +55 -8
- package/package.json +1 -1
|
@@ -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.15"
|
|
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"
|
|
@@ -1096,6 +1096,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1096
1096
|
}
|
|
1097
1097
|
let res = self.implementation.list()
|
|
1098
1098
|
for version in res {
|
|
1099
|
+
if Thread.current.isCancelled {
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1099
1102
|
self.logger.info("Deleting obsolete bundle: \(version.getId())")
|
|
1100
1103
|
let deleted = self.implementation.delete(id: version.getId())
|
|
1101
1104
|
if !deleted {
|
|
@@ -1103,17 +1106,28 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1103
1106
|
}
|
|
1104
1107
|
Thread.sleep(forTimeInterval: 0.075)
|
|
1105
1108
|
}
|
|
1106
|
-
self.implementation.cleanupDeltaCache()
|
|
1109
|
+
self.implementation.cleanupDeltaCache(threadToCheck: Thread.current)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if Thread.current.isCancelled {
|
|
1113
|
+
return
|
|
1107
1114
|
}
|
|
1108
1115
|
|
|
1109
1116
|
// Resume any DELETING leftovers from prior kills, one-by-one.
|
|
1110
1117
|
self.implementation.drainPendingDeletes()
|
|
1111
1118
|
|
|
1119
|
+
if Thread.current.isCancelled {
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1112
1123
|
// Always sweep orphan directories so incomplete prior cleanups (or failed deletes)
|
|
1113
1124
|
// cannot leave hundreds of MB behind across launches.
|
|
1114
1125
|
let allowedIds = self.implementation.allowedBundleIdsForCleanup()
|
|
1115
|
-
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds)
|
|
1116
|
-
|
|
1126
|
+
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds, threadToCheck: Thread.current)
|
|
1127
|
+
if Thread.current.isCancelled {
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
self.implementation.cleanupOrphanedTempFolders(threadToCheck: Thread.current)
|
|
1117
1131
|
|
|
1118
1132
|
if self.defaultChannelCleanupMustRetry {
|
|
1119
1133
|
self.logger.warn("Keeping the previous native build version so default channel cleanup retries")
|
|
@@ -1131,7 +1145,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1131
1145
|
}
|
|
1132
1146
|
|
|
1133
1147
|
logger.info("Waiting for cleanup to complete before starting download...")
|
|
1134
|
-
cleanupGroup.wait()
|
|
1148
|
+
let result = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1149
|
+
if result == .timedOut {
|
|
1150
|
+
logger.warn("Cleanup wait timed out after 60s, cancelling leftover cleanup")
|
|
1151
|
+
cleanupThread?.cancel()
|
|
1152
|
+
_ = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1153
|
+
return
|
|
1154
|
+
}
|
|
1135
1155
|
logger.info("Cleanup finished, proceeding with download")
|
|
1136
1156
|
}
|
|
1137
1157
|
|
|
@@ -2749,7 +2769,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2749
2769
|
guard self.previewSessionEnabled else {
|
|
2750
2770
|
return
|
|
2751
2771
|
}
|
|
2752
|
-
if let topVC = UIApplication.topViewController(),
|
|
2772
|
+
if let topVC = UIApplication.topViewController(self.bridge?.viewController),
|
|
2753
2773
|
topVC.isKind(of: UIAlertController.self) {
|
|
2754
2774
|
self.previewSessionAlertPending = true
|
|
2755
2775
|
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
@@ -2763,7 +2783,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2763
2783
|
preferredStyle: .alert
|
|
2764
2784
|
)
|
|
2765
2785
|
alert.addAction(UIAlertAction(title: "Got it", style: .default))
|
|
2766
|
-
if let topVC = UIApplication.topViewController() {
|
|
2786
|
+
if let topVC = UIApplication.topViewController(self.bridge?.viewController) {
|
|
2767
2787
|
topVC.present(alert, animated: true)
|
|
2768
2788
|
} else {
|
|
2769
2789
|
self.previewSessionAlertPending = true
|
|
@@ -4200,7 +4220,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4200
4220
|
func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
|
|
4201
4221
|
// Live update checks/downloads are user-visible work. Using `.background`
|
|
4202
4222
|
// lets the scheduler starve them for minutes while the app is active.
|
|
4203
|
-
DispatchQueue.global(qos: .
|
|
4223
|
+
DispatchQueue.global(qos: .userInitiated).async(execute: work)
|
|
4204
4224
|
}
|
|
4205
4225
|
|
|
4206
4226
|
private func beginDownloadBackgroundTask() {
|
|
@@ -4251,8 +4271,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4251
4271
|
}
|
|
4252
4272
|
|
|
4253
4273
|
self.runBackgroundDownloadWork {
|
|
4254
|
-
// Wait for cleanup to complete before starting download
|
|
4255
|
-
self.waitForCleanupIfNeeded()
|
|
4256
4274
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
4257
4275
|
self.clearDownloadInProgressState()
|
|
4258
4276
|
return
|
|
@@ -4260,7 +4278,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4260
4278
|
self.beginDownloadBackgroundTask()
|
|
4261
4279
|
self.logger.info("Check for update via \(self.updateUrl)")
|
|
4262
4280
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
4263
|
-
|
|
4281
|
+
var current = self.implementation.getCurrentBundle()
|
|
4264
4282
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
4265
4283
|
self.clearDownloadInProgressState()
|
|
4266
4284
|
self.endBackGroundTask()
|
|
@@ -4279,6 +4297,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4279
4297
|
)
|
|
4280
4298
|
return
|
|
4281
4299
|
}
|
|
4300
|
+
// File mutations wait here. getLatest already ran in parallel with cleanup.
|
|
4301
|
+
self.waitForCleanupIfNeeded()
|
|
4302
|
+
current = self.implementation.getCurrentBundle()
|
|
4282
4303
|
if res.version == "builtin" {
|
|
4283
4304
|
self.logger.info("Latest version is builtin")
|
|
4284
4305
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
@@ -318,7 +318,8 @@ import UIKit
|
|
|
318
318
|
if isSafeCacheHash(hash) && hash.count == 64 {
|
|
319
319
|
return cacheFolder.appendingPathComponent("partial_\(hash)_\(token).tmp")
|
|
320
320
|
}
|
|
321
|
-
|
|
321
|
+
let digest = CryptoCipher.shortPathKey("\(hash)|\(fileName)")
|
|
322
|
+
return cacheFolder.appendingPathComponent("partial_\(digest)_\(token).tmp")
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
private func cleanupOldManifestPartials() {
|
|
@@ -780,7 +781,7 @@ import UIKit
|
|
|
780
781
|
}
|
|
781
782
|
}
|
|
782
783
|
|
|
783
|
-
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 {
|
|
784
785
|
let fileManager = FileManager.default
|
|
785
786
|
|
|
786
787
|
switch entry.type {
|
|
@@ -803,14 +804,14 @@ import UIKit
|
|
|
803
804
|
fileHandle.closeFile()
|
|
804
805
|
}
|
|
805
806
|
|
|
806
|
-
_ = try archive.extract(entry, bufferSize:
|
|
807
|
+
_ = try archive.extract(entry, bufferSize: bufferSize, skipCRC32: true) { data in
|
|
807
808
|
if !data.isEmpty {
|
|
808
809
|
fileHandle.write(data)
|
|
809
810
|
}
|
|
810
811
|
}
|
|
811
812
|
case .symlink:
|
|
812
813
|
var linkData = Data()
|
|
813
|
-
_ = try archive.extract(entry, bufferSize:
|
|
814
|
+
_ = try archive.extract(entry, bufferSize: bufferSize, skipCRC32: true) { data in
|
|
814
815
|
linkData.append(data)
|
|
815
816
|
}
|
|
816
817
|
|
|
@@ -839,7 +840,7 @@ import UIKit
|
|
|
839
840
|
}
|
|
840
841
|
}
|
|
841
842
|
|
|
842
|
-
|
|
843
|
+
func saveDownloaded(sourceZip: URL, id: String, base: URL, notify: Bool, bufferSize: Int = CryptoCipher.ioBufferBytes()) throws {
|
|
843
844
|
try prepareFolder(source: base)
|
|
844
845
|
let destPersist: URL = base.appendingPathComponent(id)
|
|
845
846
|
let destUnZip: URL = libraryDir.appendingPathComponent(TEMP_UNZIP_PREFIX + randomString(length: 10))
|
|
@@ -886,7 +887,7 @@ import UIKit
|
|
|
886
887
|
try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true, attributes: nil)
|
|
887
888
|
}
|
|
888
889
|
|
|
889
|
-
try self.extractZipEntry(archive, entry: entry, to: destPath)
|
|
890
|
+
try self.extractZipEntry(archive, entry: entry, to: destPath, bufferSize: bufferSize)
|
|
890
891
|
|
|
891
892
|
// Update progress
|
|
892
893
|
processedEntries += 1
|
|
@@ -1735,6 +1736,38 @@ import UIKit
|
|
|
1735
1736
|
try fileManager.copyItem(at: source, to: destination)
|
|
1736
1737
|
}
|
|
1737
1738
|
|
|
1739
|
+
func copyMatchingBuiltinFilesForTests(files: [(source: URL, dest: URL, hash: String)]) throws {
|
|
1740
|
+
let queue = OperationQueue()
|
|
1741
|
+
queue.maxConcurrentOperationCount = CapgoUpdater.manifestMaxConcurrentFiles
|
|
1742
|
+
let lock = NSLock()
|
|
1743
|
+
var firstError: Error?
|
|
1744
|
+
for file in files {
|
|
1745
|
+
queue.addOperation { [weak self] in
|
|
1746
|
+
guard let self else { return }
|
|
1747
|
+
do {
|
|
1748
|
+
guard self.verifyChecksum(file: file.source, expectedHash: file.hash) else {
|
|
1749
|
+
throw NSError(
|
|
1750
|
+
domain: "CapgoInstallPerf",
|
|
1751
|
+
code: 1,
|
|
1752
|
+
userInfo: [NSLocalizedDescriptionKey: "builtin checksum mismatch"]
|
|
1753
|
+
)
|
|
1754
|
+
}
|
|
1755
|
+
try self.copyItemReplacing(from: file.source, to: file.dest)
|
|
1756
|
+
} catch {
|
|
1757
|
+
lock.lock()
|
|
1758
|
+
if firstError == nil {
|
|
1759
|
+
firstError = error
|
|
1760
|
+
}
|
|
1761
|
+
lock.unlock()
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
queue.waitUntilAllOperationsAreFinished()
|
|
1766
|
+
if let firstError {
|
|
1767
|
+
throw firstError
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1738
1771
|
/// Copy via a unique temp name then rename, so a crash cannot leave a
|
|
1739
1772
|
/// non-empty partial file that `isReusableCacheFile` would trust.
|
|
1740
1773
|
private func copyItemAtomically(from source: URL, to destination: URL) throws {
|
|
@@ -2254,6 +2287,7 @@ import UIKit
|
|
|
2254
2287
|
if !hadRegistry && !hadFolder {
|
|
2255
2288
|
logger.error("Cannot delete unknown bundle")
|
|
2256
2289
|
logger.debug("Bundle ID: \(id)")
|
|
2290
|
+
self.dequeuePendingDelete(id: id)
|
|
2257
2291
|
return false
|
|
2258
2292
|
}
|
|
2259
2293
|
|
|
@@ -2313,6 +2347,10 @@ import UIKit
|
|
|
2313
2347
|
var pendingIds = Set(self.list(raw: true).filter { $0.isDeleting() }.map { $0.getId() }.filter { !$0.isEmpty })
|
|
2314
2348
|
pendingIds.formUnion(self.getPendingDeleteIds())
|
|
2315
2349
|
for id in pendingIds {
|
|
2350
|
+
if Thread.current.isCancelled {
|
|
2351
|
+
logger.warn("drainPendingDeletes was cancelled")
|
|
2352
|
+
return
|
|
2353
|
+
}
|
|
2316
2354
|
logger.info("Resuming pending delete for bundle: \(id)")
|
|
2317
2355
|
if self.delete(id: id, removeInfo: true) {
|
|
2318
2356
|
self.dequeuePendingDelete(id: id)
|
|
@@ -2499,11 +2537,16 @@ import UIKit
|
|
|
2499
2537
|
logger.debug("Error: \(error.localizedDescription)")
|
|
2500
2538
|
}
|
|
2501
2539
|
|
|
2540
|
+
if let thread = threadToCheck, thread.isCancelled {
|
|
2541
|
+
logger.warn("cleanupOrphanedTempFolders was cancelled")
|
|
2542
|
+
return
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2502
2545
|
// Also cleanup old download temp files (package_*.tmp and update_*.dat)
|
|
2503
|
-
cleanupOldDownloadTempFiles()
|
|
2546
|
+
cleanupOldDownloadTempFiles(threadToCheck: threadToCheck)
|
|
2504
2547
|
}
|
|
2505
2548
|
|
|
2506
|
-
private func cleanupOldDownloadTempFiles() {
|
|
2549
|
+
private func cleanupOldDownloadTempFiles(threadToCheck: Thread? = nil) {
|
|
2507
2550
|
let fileManager = FileManager.default
|
|
2508
2551
|
guard let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
|
2509
2552
|
return
|
|
@@ -2514,6 +2557,10 @@ import UIKit
|
|
|
2514
2557
|
let oneHourAgo = Date().addingTimeInterval(-3600)
|
|
2515
2558
|
|
|
2516
2559
|
for url in contents {
|
|
2560
|
+
if let thread = threadToCheck, thread.isCancelled {
|
|
2561
|
+
logger.warn("cleanupOldDownloadTempFiles was cancelled")
|
|
2562
|
+
return
|
|
2563
|
+
}
|
|
2517
2564
|
let fileName = url.lastPathComponent
|
|
2518
2565
|
// Only cleanup package_*.tmp and update_*.dat files
|
|
2519
2566
|
let isDownloadTemp = (fileName.hasPrefix("package_") && fileName.hasSuffix(".tmp")) ||
|