@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.
@@ -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.13"
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
- self.implementation.cleanupOrphanedTempFolders(threadToCheck: nil)
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: .utility).async(execute: work)
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
- let current = self.implementation.getCurrentBundle()
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
- return cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)_\(token).tmp")
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: 16 * 1024, skipCRC32: true) { data in
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: 16 * 1024, skipCRC32: true) { data in
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
- private func saveDownloaded(sourceZip: URL, id: String, base: URL, notify: Bool) throws {
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")) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.51.13",
3
+ "version": "8.51.15",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",