@capgo/capacitor-updater 7.50.2 → 7.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 +53 -48
- package/android/build.gradle +6 -6
- package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +29 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +427 -116
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1108 -182
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +102 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +11 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +513 -201
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +115 -133
- package/dist/docs.json +32 -8
- package/dist/esm/definitions.d.ts +41 -17
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +124 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +806 -92
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoRawRsa.swift +36 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1020 -270
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +49 -31
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +73 -251
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +44 -20
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +28 -0
- package/package.json +13 -8
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -39
|
@@ -16,6 +16,13 @@ import Version
|
|
|
16
16
|
*/
|
|
17
17
|
@objc(CapacitorUpdaterPlugin)
|
|
18
18
|
public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
19
|
+
enum DefaultChannelPreviewSnapshot: Equatable {
|
|
20
|
+
case missing
|
|
21
|
+
case invalidated
|
|
22
|
+
case snapshot(String?)
|
|
23
|
+
case unreadable
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
lazy var logger: Logger = {
|
|
20
27
|
// Default to true for OS logging. In test environments without a bridge,
|
|
21
28
|
// this will default to true. In production, it reads from config.
|
|
@@ -85,7 +92,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
85
92
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
86
93
|
]
|
|
87
94
|
public var implementation = CapgoUpdater()
|
|
88
|
-
|
|
95
|
+
|
|
96
|
+
deinit {
|
|
97
|
+
implementation.shutdown()
|
|
98
|
+
}
|
|
99
|
+
private let pluginVersion: String = "7.51.16"
|
|
100
|
+
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
89
101
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
90
102
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
91
103
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -123,6 +135,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
123
135
|
private let previewSourceDefaultsKey = "CapacitorUpdater.previewSource"
|
|
124
136
|
private let previewSessionsDefaultsKey = "CapacitorUpdater.previewSessions"
|
|
125
137
|
private let previewSessionAlertPendingDefaultsKey = "CapacitorUpdater.previewSessionAlertPending"
|
|
138
|
+
private let defaultChannelInstallMarkerDefaultsKey = "CapacitorUpdater.defaultChannelInstallMarkerCreated"
|
|
139
|
+
private let defaultChannelInstallMarkerFilename = "CapacitorUpdater.defaultChannelInstallMarker"
|
|
140
|
+
private let defaultChannelStateFilename = "CapacitorUpdater.defaultChannelState"
|
|
141
|
+
private let defaultChannelPreviewSnapshotFilename = "CapacitorUpdater.defaultChannelPreviewSnapshot"
|
|
126
142
|
private let previewDeepLinkScheme = "capgo"
|
|
127
143
|
private let previewDeepLinkRootComponent = "preview"
|
|
128
144
|
private let previewDeepLinkChannelComponent = "channel"
|
|
@@ -135,6 +151,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
135
151
|
private var currentBuildVersion: String = "0"
|
|
136
152
|
private var autoUpdate = false
|
|
137
153
|
private var autoUpdateMode = CapacitorUpdaterPlugin.autoUpdateModeOff
|
|
154
|
+
private var launchStartReported = false
|
|
155
|
+
private var launchReadyReported = false
|
|
138
156
|
private var appReadyTimeout = 10000
|
|
139
157
|
private var appReadyCheck: DispatchWorkItem?
|
|
140
158
|
private var resetWhenUpdate = true
|
|
@@ -160,6 +178,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
160
178
|
private var autoDeleteFailed = false
|
|
161
179
|
private var autoDeletePrevious = false
|
|
162
180
|
var allowSetDefaultChannel = true
|
|
181
|
+
var persistDefaultChannelOnReinstall = true
|
|
163
182
|
private var keepUrlPathAfterReload = false
|
|
164
183
|
private var backgroundWork: DispatchWorkItem?
|
|
165
184
|
private var taskRunning = false
|
|
@@ -172,8 +191,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
172
191
|
|
|
173
192
|
// Lock to ensure cleanup completes before downloads start
|
|
174
193
|
private let cleanupLock = NSLock()
|
|
194
|
+
// Short lock for cleanupComplete/cleanupTimedOut only. Never hold across cleanupGroup.wait.
|
|
195
|
+
private let cleanupStateLock = NSLock()
|
|
196
|
+
private let defaultChannelStateLock = NSLock()
|
|
197
|
+
private let cleanupGroup = DispatchGroup()
|
|
175
198
|
private var cleanupComplete = false
|
|
199
|
+
private var cleanupTimedOut = false
|
|
176
200
|
private var cleanupThread: Thread?
|
|
201
|
+
private var defaultChannelCleanupMustRetry = false
|
|
177
202
|
private var persistCustomId = false
|
|
178
203
|
private var persistModifyUrl = false
|
|
179
204
|
private var allowManualBundleError = false
|
|
@@ -191,6 +216,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
191
216
|
private var isLeavingPreviewForIncomingLink = false
|
|
192
217
|
private var previewTransitionClearWorkItem: DispatchWorkItem?
|
|
193
218
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
219
|
+
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
220
|
+
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
221
|
+
private var pendingNotifyAppReady = false
|
|
222
|
+
private let semaphoreWaitTestingLock = NSLock()
|
|
223
|
+
private var didEnterSemaphoreWaitForTesting = false
|
|
194
224
|
|
|
195
225
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
196
226
|
|
|
@@ -216,6 +246,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
216
246
|
self.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId()
|
|
217
247
|
persistCustomId = getConfig().getBoolean("persistCustomId", false)
|
|
218
248
|
allowSetDefaultChannel = getConfig().getBoolean("allowSetDefaultChannel", true)
|
|
249
|
+
persistDefaultChannelOnReinstall = getConfig().getBoolean("persistDefaultChannelOnReinstall", true)
|
|
219
250
|
if persistCustomId {
|
|
220
251
|
let storedCustomId = UserDefaults.standard.string(forKey: customIdDefaultsKey) ?? ""
|
|
221
252
|
if !storedCustomId.isEmpty {
|
|
@@ -331,14 +362,61 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
331
362
|
logger.info("Loaded persisted channelUrl")
|
|
332
363
|
}
|
|
333
364
|
}
|
|
365
|
+
implementation.restorePendingStats()
|
|
334
366
|
|
|
335
|
-
|
|
336
|
-
|
|
367
|
+
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
368
|
+
let defaultChannelPersistenceDisabled = !persistDefaultChannelOnReinstall
|
|
369
|
+
let restoredReinstall = defaultChannelPersistenceDisabled && self.isRestoredReinstall()
|
|
370
|
+
var installMarkerCanBePrepared = true
|
|
371
|
+
if shouldClearPersistedDefaultChannel(
|
|
372
|
+
nativeBuildVersionChanged: nativeBuildVersionChanged,
|
|
373
|
+
resetWhenUpdate: resetWhenUpdate,
|
|
374
|
+
restoredReinstall: restoredReinstall
|
|
375
|
+
) {
|
|
376
|
+
installMarkerCanBePrepared = clearPersistedDefaultChannel()
|
|
377
|
+
if installMarkerCanBePrepared {
|
|
378
|
+
logger.info("Cleared persisted defaultChannel because reinstall persistence is disabled")
|
|
379
|
+
} else {
|
|
380
|
+
logger.warn("Cannot durably clear persisted defaultChannel")
|
|
381
|
+
self.defaultChannelCleanupMustRetry = true
|
|
382
|
+
self.invalidateDefaultChannelInstallMarker()
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if defaultChannelPersistenceDisabled && installMarkerCanBePrepared {
|
|
386
|
+
self.prepareDefaultChannelInstallMarker()
|
|
387
|
+
}
|
|
388
|
+
if !previewSessionEnabled,
|
|
389
|
+
!defaultChannelCleanupMustRetry,
|
|
390
|
+
self.hasPendingDefaultChannelPreviewSnapshot(),
|
|
391
|
+
!self.restorePreviewPreviousDefaultChannel() {
|
|
392
|
+
logger.warn("Default channel preview restore remains pending")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let configDefaultChannel = getConfig().getString("defaultChannel", "")!
|
|
396
|
+
let storedDefaultChannel = UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
397
|
+
let stateFile = self.defaultChannelStateFile()
|
|
398
|
+
let state = stateFile.map { self.defaultChannelState(file: $0) }
|
|
399
|
+
if defaultChannelCleanupMustRetry {
|
|
400
|
+
implementation.defaultChannel = configDefaultChannel
|
|
401
|
+
logger.warn("Using configured defaultChannel until persisted cleanup can retry")
|
|
402
|
+
} else if let state = state, state.exists, state.isReadable {
|
|
403
|
+
self.reconcileDefaultChannelDefaults(with: state.channel)
|
|
404
|
+
implementation.defaultChannel = state.channel ?? configDefaultChannel
|
|
405
|
+
logger.info("Loaded persisted defaultChannel from local state")
|
|
406
|
+
} else if defaultChannelPersistenceDisabled, state?.exists == true {
|
|
407
|
+
implementation.defaultChannel = configDefaultChannel
|
|
408
|
+
logger.warn("Ignoring unreadable persisted defaultChannel while reinstall persistence is disabled")
|
|
409
|
+
} else if let storedDefaultChannel = storedDefaultChannel {
|
|
337
410
|
implementation.defaultChannel = storedDefaultChannel
|
|
338
411
|
logger.info("Loaded persisted defaultChannel from setChannel()")
|
|
339
412
|
} else {
|
|
340
|
-
implementation.defaultChannel =
|
|
413
|
+
implementation.defaultChannel = configDefaultChannel
|
|
341
414
|
}
|
|
415
|
+
if !defaultChannelCleanupMustRetry,
|
|
416
|
+
state?.exists != true || (!defaultChannelPersistenceDisabled && state?.isReadable != true) {
|
|
417
|
+
_ = self.persistDefaultChannelStateFromDefaults()
|
|
418
|
+
}
|
|
419
|
+
self.reportAppLaunchStart()
|
|
342
420
|
self.implementation.autoReset()
|
|
343
421
|
let appHealthTracker = AppHealthTracker(implementation: self.implementation)
|
|
344
422
|
self.appHealthTracker = appHealthTracker
|
|
@@ -347,16 +425,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
347
425
|
|
|
348
426
|
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
349
427
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
350
|
-
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
351
428
|
if nativeBuildVersionChanged {
|
|
352
429
|
self.clearPreviewSessionForNativeBuildChange()
|
|
353
430
|
}
|
|
354
431
|
self.leavePreviewSessionForLaunchURLIfNeeded()
|
|
355
432
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
self
|
|
433
|
+
// Downloads (including shake-menu / CapgoUpdater entry points) wait on this gate.
|
|
434
|
+
self.implementation.beforeDownload = { [weak self] in
|
|
435
|
+
try self?.waitForCleanupIfNeeded()
|
|
359
436
|
}
|
|
437
|
+
// Always run async cleanup: delete obsolete bundles on native update (when enabled)
|
|
438
|
+
// and sweep orphan directories every launch. Must not block app startup.
|
|
439
|
+
if !resetWhenUpdate {
|
|
440
|
+
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
441
|
+
UserDefaults.standard.synchronize()
|
|
442
|
+
}
|
|
443
|
+
let didResetCurrentBundle = resetWhenUpdate ? self.resetCurrentBundleForNativeBuildChangeIfNeeded() : false
|
|
444
|
+
self.cleanupObsoleteVersions(
|
|
445
|
+
resetWhenUpdate: resetWhenUpdate,
|
|
446
|
+
didResetCurrentBundle: didResetCurrentBundle
|
|
447
|
+
)
|
|
360
448
|
self.reportNativeVersionStatsIfChanged()
|
|
361
449
|
|
|
362
450
|
// Load the server
|
|
@@ -511,6 +599,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
511
599
|
|
|
512
600
|
private func semaphoreWait(waitTime: Int) {
|
|
513
601
|
// print("\\(CapgoUpdater.TAG) semaphoreWait \\(waitTime)")
|
|
602
|
+
semaphoreWaitTestingLock.lock()
|
|
603
|
+
didEnterSemaphoreWaitForTesting = true
|
|
604
|
+
semaphoreWaitTestingLock.unlock()
|
|
514
605
|
let result = semaphoreReady.wait(timeout: .now() + .milliseconds(waitTime))
|
|
515
606
|
if result == .timedOut {
|
|
516
607
|
logger.error("Semaphore wait timed out after \(waitTime)ms")
|
|
@@ -518,6 +609,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
518
609
|
}
|
|
519
610
|
|
|
520
611
|
private func semaphoreUp() {
|
|
612
|
+
pendingNotifyAppReady = true
|
|
521
613
|
DispatchQueue.global().async {
|
|
522
614
|
self.semaphoreWait(waitTime: 0)
|
|
523
615
|
}
|
|
@@ -531,6 +623,327 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
531
623
|
UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
532
624
|
}
|
|
533
625
|
|
|
626
|
+
func shouldClearPersistedDefaultChannel(
|
|
627
|
+
nativeBuildVersionChanged: Bool,
|
|
628
|
+
resetWhenUpdate: Bool,
|
|
629
|
+
restoredReinstall: Bool
|
|
630
|
+
) -> Bool {
|
|
631
|
+
!persistDefaultChannelOnReinstall && (restoredReinstall || (resetWhenUpdate && nativeBuildVersionChanged))
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
func clearPersistedDefaultChannel() -> Bool {
|
|
635
|
+
guard let stateFile = self.defaultChannelStateFile(),
|
|
636
|
+
let previewSnapshotFile = self.defaultChannelPreviewSnapshotFile() else {
|
|
637
|
+
return false
|
|
638
|
+
}
|
|
639
|
+
return self.clearPersistedDefaultChannel(
|
|
640
|
+
stateFile: stateFile,
|
|
641
|
+
previewSnapshotFile: previewSnapshotFile
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
func clearPersistedDefaultChannel(stateFile: URL, previewSnapshotFile: URL) -> Bool {
|
|
646
|
+
defaultChannelStateLock.lock()
|
|
647
|
+
defer { defaultChannelStateLock.unlock() }
|
|
648
|
+
do {
|
|
649
|
+
try self.persistDefaultChannelStateWithoutLock(channel: nil, file: stateFile)
|
|
650
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
651
|
+
channel: nil,
|
|
652
|
+
isValid: false,
|
|
653
|
+
file: previewSnapshotFile
|
|
654
|
+
)
|
|
655
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelDefaultsKey)
|
|
656
|
+
UserDefaults.standard.removeObject(forKey: previewPreviousDefaultChannelDefaultsKey)
|
|
657
|
+
UserDefaults.standard.removeObject(forKey: previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
658
|
+
return true
|
|
659
|
+
} catch {
|
|
660
|
+
logger.warn("Cannot persist cleared default channel state: \(error.localizedDescription)")
|
|
661
|
+
return false
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private func defaultChannelStateFile() -> URL? {
|
|
666
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
667
|
+
.appendingPathComponent(defaultChannelStateFilename)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private func defaultChannelPreviewSnapshotFile() -> URL? {
|
|
671
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
672
|
+
.appendingPathComponent(defaultChannelPreviewSnapshotFilename)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
func defaultChannelState(file: URL) -> (exists: Bool, channel: String?, isReadable: Bool) {
|
|
676
|
+
defaultChannelStateLock.lock()
|
|
677
|
+
defer { defaultChannelStateLock.unlock() }
|
|
678
|
+
return self.defaultChannelStateWithoutLock(file: file)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private func defaultChannelStateWithoutLock(file: URL) -> (exists: Bool, channel: String?, isReadable: Bool) {
|
|
682
|
+
let fileManager = FileManager.default
|
|
683
|
+
guard fileManager.fileExists(atPath: file.path) else {
|
|
684
|
+
return (false, nil, true)
|
|
685
|
+
}
|
|
686
|
+
do {
|
|
687
|
+
let data = try Data(contentsOf: file)
|
|
688
|
+
guard !data.isEmpty else {
|
|
689
|
+
return (true, nil, true)
|
|
690
|
+
}
|
|
691
|
+
guard let channel = String(data: data, encoding: .utf8) else {
|
|
692
|
+
logger.warn("Cannot decode persisted default channel state")
|
|
693
|
+
return (true, nil, false)
|
|
694
|
+
}
|
|
695
|
+
return (true, channel, true)
|
|
696
|
+
} catch {
|
|
697
|
+
logger.warn("Cannot read persisted default channel state: \(error.localizedDescription)")
|
|
698
|
+
return (true, nil, false)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
func persistDefaultChannelState(channel: String?, file: URL) throws {
|
|
703
|
+
defaultChannelStateLock.lock()
|
|
704
|
+
defer { defaultChannelStateLock.unlock() }
|
|
705
|
+
try self.persistDefaultChannelStateWithoutLock(channel: channel, file: file)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private func persistDefaultChannelStateWithoutLock(channel: String?, file: URL) throws {
|
|
709
|
+
let data = channel.map { Data($0.utf8) } ?? Data()
|
|
710
|
+
try self.persistBackupExcludedFile(file: file, data: data, overwrite: true)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
func defaultChannelPreviewSnapshot(file: URL) -> DefaultChannelPreviewSnapshot {
|
|
714
|
+
defaultChannelStateLock.lock()
|
|
715
|
+
defer { defaultChannelStateLock.unlock() }
|
|
716
|
+
return self.defaultChannelPreviewSnapshotWithoutLock(file: file)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private func defaultChannelPreviewSnapshotWithoutLock(file: URL) -> DefaultChannelPreviewSnapshot {
|
|
720
|
+
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
721
|
+
return .missing
|
|
722
|
+
}
|
|
723
|
+
do {
|
|
724
|
+
let data = try Data(contentsOf: file)
|
|
725
|
+
guard let kind = data.first else {
|
|
726
|
+
return .unreadable
|
|
727
|
+
}
|
|
728
|
+
switch kind {
|
|
729
|
+
case 0 where data.count == 1:
|
|
730
|
+
return .invalidated
|
|
731
|
+
case 1 where data.count == 1:
|
|
732
|
+
return .snapshot(nil)
|
|
733
|
+
case 2:
|
|
734
|
+
guard let channel = String(data: Data(data.dropFirst()), encoding: .utf8) else {
|
|
735
|
+
return .unreadable
|
|
736
|
+
}
|
|
737
|
+
return .snapshot(channel)
|
|
738
|
+
default:
|
|
739
|
+
return .unreadable
|
|
740
|
+
}
|
|
741
|
+
} catch {
|
|
742
|
+
logger.warn("Cannot read default channel preview snapshot: \(error.localizedDescription)")
|
|
743
|
+
return .unreadable
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
func persistDefaultChannelPreviewSnapshot(channel: String?, file: URL) throws {
|
|
748
|
+
defaultChannelStateLock.lock()
|
|
749
|
+
defer { defaultChannelStateLock.unlock() }
|
|
750
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
751
|
+
channel: channel,
|
|
752
|
+
isValid: true,
|
|
753
|
+
file: file
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
func invalidateDefaultChannelPreviewSnapshot(file: URL) throws {
|
|
758
|
+
defaultChannelStateLock.lock()
|
|
759
|
+
defer { defaultChannelStateLock.unlock() }
|
|
760
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
761
|
+
channel: nil,
|
|
762
|
+
isValid: false,
|
|
763
|
+
file: file
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private func persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
768
|
+
channel: String?,
|
|
769
|
+
isValid: Bool,
|
|
770
|
+
file: URL
|
|
771
|
+
) throws {
|
|
772
|
+
var data = Data()
|
|
773
|
+
if !isValid {
|
|
774
|
+
data.append(0)
|
|
775
|
+
} else if let channel = channel {
|
|
776
|
+
data.append(2)
|
|
777
|
+
data.append(contentsOf: channel.utf8)
|
|
778
|
+
} else {
|
|
779
|
+
data.append(1)
|
|
780
|
+
}
|
|
781
|
+
try self.persistBackupExcludedFile(file: file, data: data, overwrite: true)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private func persistDefaultChannelPreviewSnapshot(channel: String?) -> Bool {
|
|
785
|
+
guard let file = self.defaultChannelPreviewSnapshotFile() else {
|
|
786
|
+
return false
|
|
787
|
+
}
|
|
788
|
+
do {
|
|
789
|
+
try self.persistDefaultChannelPreviewSnapshot(channel: channel, file: file)
|
|
790
|
+
return true
|
|
791
|
+
} catch {
|
|
792
|
+
logger.warn("Cannot persist default channel preview snapshot: \(error.localizedDescription)")
|
|
793
|
+
return false
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private func hasPendingDefaultChannelPreviewSnapshot() -> Bool {
|
|
798
|
+
guard let file = self.defaultChannelPreviewSnapshotFile() else {
|
|
799
|
+
return false
|
|
800
|
+
}
|
|
801
|
+
switch self.defaultChannelPreviewSnapshot(file: file) {
|
|
802
|
+
case .snapshot, .unreadable:
|
|
803
|
+
return true
|
|
804
|
+
case .missing, .invalidated:
|
|
805
|
+
return false
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
@discardableResult
|
|
810
|
+
func persistDefaultChannelStateFromDefaults() -> Bool {
|
|
811
|
+
guard let file = self.defaultChannelStateFile() else {
|
|
812
|
+
logger.warn("Cannot locate default channel state file")
|
|
813
|
+
return false
|
|
814
|
+
}
|
|
815
|
+
return self.persistDefaultChannelStateFromDefaults(stateFile: file)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
@discardableResult
|
|
819
|
+
func persistDefaultChannelStateFromDefaults(stateFile: URL) -> Bool {
|
|
820
|
+
defaultChannelStateLock.lock()
|
|
821
|
+
defer { defaultChannelStateLock.unlock() }
|
|
822
|
+
do {
|
|
823
|
+
try self.persistDefaultChannelStateWithoutLock(
|
|
824
|
+
channel: UserDefaults.standard.string(forKey: defaultChannelDefaultsKey),
|
|
825
|
+
file: stateFile
|
|
826
|
+
)
|
|
827
|
+
return true
|
|
828
|
+
} catch {
|
|
829
|
+
do {
|
|
830
|
+
if FileManager.default.fileExists(atPath: stateFile.path) {
|
|
831
|
+
try FileManager.default.removeItem(at: stateFile)
|
|
832
|
+
}
|
|
833
|
+
logger.warn("Cannot persist default channel state; falling back to UserDefaults: \(error.localizedDescription)")
|
|
834
|
+
return true
|
|
835
|
+
} catch let invalidationError {
|
|
836
|
+
logger.warn("Cannot persist or invalidate default channel state: \(invalidationError.localizedDescription)")
|
|
837
|
+
return false
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private func persistedDefaultChannel() -> String? {
|
|
843
|
+
guard let file = self.defaultChannelStateFile() else {
|
|
844
|
+
return UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
845
|
+
}
|
|
846
|
+
return self.persistedDefaultChannel(stateFile: file)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
func persistedDefaultChannel(stateFile: URL) -> String? {
|
|
850
|
+
let state = self.defaultChannelState(file: stateFile)
|
|
851
|
+
if state.exists {
|
|
852
|
+
if state.isReadable {
|
|
853
|
+
return state.channel
|
|
854
|
+
}
|
|
855
|
+
if !persistDefaultChannelOnReinstall {
|
|
856
|
+
return nil
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private func reconcileDefaultChannelDefaults(with channel: String?) {
|
|
863
|
+
if let channel = channel {
|
|
864
|
+
UserDefaults.standard.set(channel, forKey: defaultChannelDefaultsKey)
|
|
865
|
+
} else {
|
|
866
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelDefaultsKey)
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private func defaultChannelInstallMarker() -> URL? {
|
|
871
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
872
|
+
.appendingPathComponent(defaultChannelInstallMarkerFilename)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private func invalidateDefaultChannelInstallMarker() {
|
|
876
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
877
|
+
return
|
|
878
|
+
}
|
|
879
|
+
do {
|
|
880
|
+
try self.invalidateDefaultChannelInstallMarker(marker: marker)
|
|
881
|
+
} catch {
|
|
882
|
+
logger.warn("Cannot invalidate default channel install marker for cleanup retry: \(error.localizedDescription)")
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
func invalidateDefaultChannelInstallMarker(marker: URL) throws {
|
|
887
|
+
let fileManager = FileManager.default
|
|
888
|
+
if fileManager.fileExists(atPath: marker.path) {
|
|
889
|
+
try fileManager.removeItem(at: marker)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
func isRestoredReinstall() -> Bool {
|
|
894
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
895
|
+
return false
|
|
896
|
+
}
|
|
897
|
+
return self.isRestoredReinstall(
|
|
898
|
+
marker: marker,
|
|
899
|
+
markerWasCreated: UserDefaults.standard.bool(forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
func isRestoredReinstall(marker: URL, markerWasCreated: Bool) -> Bool {
|
|
904
|
+
markerWasCreated && !FileManager.default.fileExists(atPath: marker.path)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private func prepareDefaultChannelInstallMarker() {
|
|
908
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
self.prepareDefaultChannelInstallMarker(
|
|
912
|
+
marker: marker,
|
|
913
|
+
markerWasCreated: UserDefaults.standard.bool(forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
func prepareDefaultChannelInstallMarker(marker: URL, markerWasCreated: Bool) {
|
|
918
|
+
do {
|
|
919
|
+
try self.persistBackupExcludedFile(file: marker, data: Data())
|
|
920
|
+
if !markerWasCreated {
|
|
921
|
+
UserDefaults.standard.set(true, forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
922
|
+
UserDefaults.standard.synchronize()
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
try? FileManager.default.removeItem(at: marker)
|
|
926
|
+
UserDefaults.standard.set(false, forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
927
|
+
UserDefaults.standard.synchronize()
|
|
928
|
+
logger.warn("Cannot prepare default channel install marker: \(error.localizedDescription)")
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private func persistBackupExcludedFile(file: URL, data: Data, overwrite: Bool = false) throws {
|
|
933
|
+
let fileManager = FileManager.default
|
|
934
|
+
if overwrite || !fileManager.fileExists(atPath: file.path) {
|
|
935
|
+
try fileManager.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
936
|
+
try data.write(to: file, options: .atomic)
|
|
937
|
+
}
|
|
938
|
+
let currentResourceValues = try file.resourceValues(forKeys: [.isExcludedFromBackupKey])
|
|
939
|
+
if currentResourceValues.isExcludedFromBackup != true {
|
|
940
|
+
var resourceValues = URLResourceValues()
|
|
941
|
+
resourceValues.isExcludedFromBackup = true
|
|
942
|
+
var mutableFile = file
|
|
943
|
+
try mutableFile.setResourceValues(resourceValues)
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
534
947
|
func hasNativeBuildVersionChanged() -> Bool {
|
|
535
948
|
let previous = self.storedNativeBuildVersion()
|
|
536
949
|
return previous != "0" && self.currentBuildVersion != previous
|
|
@@ -629,12 +1042,35 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
629
1042
|
return true
|
|
630
1043
|
}
|
|
631
1044
|
|
|
632
|
-
private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
|
|
1045
|
+
private func cleanupObsoleteVersions(resetWhenUpdate: Bool = true, didResetCurrentBundle: Bool = false) {
|
|
1046
|
+
// Enter before publishing incomplete state so waiters cannot hit an empty group.
|
|
1047
|
+
self.cleanupStateLock.lock()
|
|
1048
|
+
self.cleanupComplete = false
|
|
1049
|
+
self.cleanupTimedOut = false
|
|
1050
|
+
self.cleanupGroup.enter()
|
|
1051
|
+
self.cleanupStateLock.unlock()
|
|
633
1052
|
cleanupThread = Thread {
|
|
1053
|
+
let bgTaskLock = NSLock()
|
|
1054
|
+
var cleanupBackgroundTask = UIBackgroundTaskIdentifier.invalid
|
|
1055
|
+
let endCleanupBackgroundTask = {
|
|
1056
|
+
bgTaskLock.lock()
|
|
1057
|
+
defer { bgTaskLock.unlock() }
|
|
1058
|
+
if cleanupBackgroundTask != .invalid {
|
|
1059
|
+
UIApplication.shared.endBackgroundTask(cleanupBackgroundTask)
|
|
1060
|
+
cleanupBackgroundTask = .invalid
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
cleanupBackgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CapgoBundleCleanup") {
|
|
1064
|
+
endCleanupBackgroundTask()
|
|
1065
|
+
}
|
|
634
1066
|
self.cleanupLock.lock()
|
|
635
1067
|
defer {
|
|
1068
|
+
self.cleanupStateLock.lock()
|
|
636
1069
|
self.cleanupComplete = true
|
|
1070
|
+
self.cleanupStateLock.unlock()
|
|
637
1071
|
self.cleanupLock.unlock()
|
|
1072
|
+
self.cleanupGroup.leave()
|
|
1073
|
+
endCleanupBackgroundTask()
|
|
638
1074
|
self.logger.info("Cleanup complete")
|
|
639
1075
|
}
|
|
640
1076
|
|
|
@@ -665,67 +1101,92 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
665
1101
|
// 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
|
|
666
1102
|
|
|
667
1103
|
let previous = self.storedNativeBuildVersion()
|
|
668
|
-
|
|
1104
|
+
let nativeVersionChanged = previous != "0" && self.currentBuildVersion != previous
|
|
1105
|
+
if resetWhenUpdate && nativeVersionChanged {
|
|
669
1106
|
if !didResetCurrentBundle {
|
|
670
1107
|
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
|
|
671
1108
|
self.implementation.reset(isInternal: true)
|
|
672
1109
|
}
|
|
673
1110
|
let res = self.implementation.list()
|
|
674
1111
|
for version in res {
|
|
675
|
-
// Check if thread was cancelled
|
|
676
1112
|
if Thread.current.isCancelled {
|
|
677
|
-
self.logger.warn("Cleanup was cancelled, stopping")
|
|
678
1113
|
return
|
|
679
1114
|
}
|
|
680
1115
|
self.logger.info("Deleting obsolete bundle: \(version.getId())")
|
|
681
|
-
let
|
|
682
|
-
if !
|
|
1116
|
+
let deleted = self.implementation.delete(id: version.getId())
|
|
1117
|
+
if !deleted {
|
|
683
1118
|
self.logger.error("Delete failed, id \(version.getId()) doesn't exist")
|
|
684
1119
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
let storedBundles = self.implementation.list(raw: true)
|
|
688
|
-
let allowedIds = Set(storedBundles.compactMap { info -> String? in
|
|
689
|
-
let id = info.getId()
|
|
690
|
-
return id.isEmpty ? nil : id
|
|
691
|
-
})
|
|
692
|
-
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds, threadToCheck: Thread.current)
|
|
693
|
-
self.implementation.cleanupOrphanedTempFolders(threadToCheck: Thread.current)
|
|
694
|
-
|
|
695
|
-
// Check again before the expensive delta cache cleanup
|
|
696
|
-
if Thread.current.isCancelled {
|
|
697
|
-
self.logger.warn("Cleanup was cancelled before delta cache cleanup")
|
|
698
|
-
return
|
|
1120
|
+
Thread.sleep(forTimeInterval: 0.075)
|
|
699
1121
|
}
|
|
700
1122
|
self.implementation.cleanupDeltaCache(threadToCheck: Thread.current)
|
|
701
1123
|
}
|
|
702
|
-
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
703
|
-
UserDefaults.standard.synchronize()
|
|
704
|
-
}
|
|
705
|
-
cleanupThread?.start()
|
|
706
1124
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1125
|
+
if Thread.current.isCancelled {
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Resume any DELETING leftovers from prior kills, one-by-one.
|
|
1130
|
+
self.implementation.drainPendingDeletes()
|
|
1131
|
+
|
|
1132
|
+
if Thread.current.isCancelled {
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Always sweep orphan directories so incomplete prior cleanups (or failed deletes)
|
|
1137
|
+
// cannot leave hundreds of MB behind across launches.
|
|
1138
|
+
let allowedIds = self.implementation.allowedBundleIdsForCleanup()
|
|
1139
|
+
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds, threadToCheck: Thread.current)
|
|
1140
|
+
if Thread.current.isCancelled {
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
self.implementation.cleanupOrphanedTempFolders(threadToCheck: Thread.current)
|
|
1144
|
+
|
|
1145
|
+
if self.defaultChannelCleanupMustRetry {
|
|
1146
|
+
self.logger.warn("Keeping the previous native build version so default channel cleanup retries")
|
|
1147
|
+
} else {
|
|
1148
|
+
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
1149
|
+
UserDefaults.standard.synchronize()
|
|
714
1150
|
}
|
|
715
1151
|
}
|
|
1152
|
+
cleanupThread?.start()
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private func cleanupTimeoutError() -> NSError {
|
|
1156
|
+
NSError(
|
|
1157
|
+
domain: "CapacitorUpdater",
|
|
1158
|
+
code: 1,
|
|
1159
|
+
userInfo: [NSLocalizedDescriptionKey: "Cleanup did not finish before download"]
|
|
1160
|
+
)
|
|
716
1161
|
}
|
|
717
1162
|
|
|
718
|
-
private func waitForCleanupIfNeeded() {
|
|
719
|
-
|
|
1163
|
+
private func waitForCleanupIfNeeded() throws {
|
|
1164
|
+
cleanupStateLock.lock()
|
|
1165
|
+
let alreadyDone = cleanupComplete
|
|
1166
|
+
let alreadyTimedOut = cleanupTimedOut
|
|
1167
|
+
cleanupStateLock.unlock()
|
|
1168
|
+
if alreadyDone {
|
|
720
1169
|
return // Already done, no need to wait
|
|
721
1170
|
}
|
|
1171
|
+
if alreadyTimedOut {
|
|
1172
|
+
throw cleanupTimeoutError()
|
|
1173
|
+
}
|
|
722
1174
|
|
|
723
1175
|
logger.info("Waiting for cleanup to complete before starting download...")
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1176
|
+
let result = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1177
|
+
if result == .timedOut {
|
|
1178
|
+
logger.warn("Cleanup wait timed out after 60s, cancelling leftover cleanup")
|
|
1179
|
+
cleanupThread?.cancel()
|
|
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
|
+
}
|
|
1188
|
+
return
|
|
1189
|
+
}
|
|
729
1190
|
logger.info("Cleanup finished, proceeding with download")
|
|
730
1191
|
}
|
|
731
1192
|
|
|
@@ -1142,6 +1603,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1142
1603
|
}
|
|
1143
1604
|
|
|
1144
1605
|
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
1606
|
+
// Manual/preview downloads must wait too - launch orphan sweep can delete their temps.
|
|
1607
|
+
try self.waitForCleanupIfNeeded()
|
|
1145
1608
|
guard let url = URL(string: urlString) else {
|
|
1146
1609
|
throw makePreviewError("Invalid download URL")
|
|
1147
1610
|
}
|
|
@@ -1293,6 +1756,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1293
1756
|
|
|
1294
1757
|
let performReload: () -> Bool = {
|
|
1295
1758
|
guard self.applyCurrentBundleToBridge(bridge) else {
|
|
1759
|
+
self.clearPendingNotifyAppReady()
|
|
1296
1760
|
return false
|
|
1297
1761
|
}
|
|
1298
1762
|
self.checkAppReady()
|
|
@@ -1380,6 +1844,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1380
1844
|
}
|
|
1381
1845
|
}
|
|
1382
1846
|
|
|
1847
|
+
private func queueBundleForNextBackgroundInstall(_ next: BundleInfo) -> Bool {
|
|
1848
|
+
guard self.implementation.setNextBundle(next: next.getId()) else {
|
|
1849
|
+
self.logger.error("Failed to queue downloaded bundle as next: \(next.toString())")
|
|
1850
|
+
return false
|
|
1851
|
+
}
|
|
1852
|
+
return true
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1383
1855
|
private func applyDownloadedBundleForDirectUpdate(_ next: BundleInfo) -> Bool {
|
|
1384
1856
|
let previousState = self.implementation.captureResetState()
|
|
1385
1857
|
let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
|
|
@@ -1497,8 +1969,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1497
1969
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
1498
1970
|
}
|
|
1499
1971
|
|
|
1972
|
+
let previousDefaultChannel = self.persistedDefaultChannel()
|
|
1973
|
+
guard self.persistDefaultChannelPreviewSnapshot(channel: previousDefaultChannel) else {
|
|
1974
|
+
logger.error("Could not durably save the default channel preview snapshot")
|
|
1975
|
+
return false
|
|
1976
|
+
}
|
|
1500
1977
|
UserDefaults.standard.set(self.implementation.appId, forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1501
|
-
if let previousDefaultChannel =
|
|
1978
|
+
if let previousDefaultChannel = previousDefaultChannel {
|
|
1502
1979
|
UserDefaults.standard.set(previousDefaultChannel, forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1503
1980
|
UserDefaults.standard.set(true, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1504
1981
|
} else {
|
|
@@ -1946,7 +2423,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1946
2423
|
?? self.getBooleanConfig("allowShakeChannelSelector", defaultValue: false)
|
|
1947
2424
|
self.restorePreviewPreviousNextBundle()
|
|
1948
2425
|
self.restorePreviewPreviousAppId()
|
|
1949
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2426
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2427
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2428
|
+
}
|
|
1950
2429
|
|
|
1951
2430
|
self.previewSessionEnabled = false
|
|
1952
2431
|
self.previewSessionAlertPending = false
|
|
@@ -1973,7 +2452,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1973
2452
|
|
|
1974
2453
|
self.restorePreviewPreviousNextBundle()
|
|
1975
2454
|
self.restorePreviewPreviousAppId()
|
|
1976
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2455
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2456
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2457
|
+
}
|
|
1977
2458
|
self.previewSessionEnabled = false
|
|
1978
2459
|
self.previewSessionAlertPending = false
|
|
1979
2460
|
self.isLeavingPreviewForIncomingLink = false
|
|
@@ -2001,14 +2482,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2001
2482
|
}
|
|
2002
2483
|
|
|
2003
2484
|
private func clearPreviewSessionPreferences() {
|
|
2485
|
+
let keepDefaultChannelSnapshotFallback = self.hasPendingDefaultChannelPreviewSnapshot()
|
|
2004
2486
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2005
2487
|
UserDefaults.standard.removeObject(forKey: self.previewSessionDefaultsKey)
|
|
2006
2488
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeMenuDefaultsKey)
|
|
2007
2489
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
2008
2490
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
2009
2491
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousAppIdDefaultsKey)
|
|
2010
|
-
|
|
2011
|
-
|
|
2492
|
+
if !keepDefaultChannelSnapshotFallback {
|
|
2493
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
2494
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
2495
|
+
}
|
|
2012
2496
|
UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
|
|
2013
2497
|
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
2014
2498
|
UserDefaults.standard.removeObject(forKey: self.previewNameDefaultsKey)
|
|
@@ -2026,21 +2510,95 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2026
2510
|
logger.info("Restored appId after preview: \(previousAppId)")
|
|
2027
2511
|
}
|
|
2028
2512
|
|
|
2029
|
-
private func restorePreviewPreviousDefaultChannel() {
|
|
2030
|
-
|
|
2031
|
-
|
|
2513
|
+
private func restorePreviewPreviousDefaultChannel() -> Bool {
|
|
2514
|
+
guard !defaultChannelCleanupMustRetry else {
|
|
2515
|
+
logger.warn("Skipping default channel preview restore until cleanup retries")
|
|
2516
|
+
return false
|
|
2517
|
+
}
|
|
2518
|
+
guard let stateFile = self.defaultChannelStateFile(),
|
|
2519
|
+
let previewSnapshotFile = self.defaultChannelPreviewSnapshotFile() else {
|
|
2520
|
+
return false
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
let previousDefaultChannel: String?
|
|
2524
|
+
switch self.defaultChannelPreviewSnapshot(file: previewSnapshotFile) {
|
|
2525
|
+
case .invalidated:
|
|
2526
|
+
return true
|
|
2527
|
+
case let .snapshot(channel):
|
|
2528
|
+
previousDefaultChannel = channel
|
|
2529
|
+
case .missing:
|
|
2530
|
+
let hadPreviousDefaultChannel = UserDefaults.standard.object(
|
|
2531
|
+
forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey
|
|
2532
|
+
) as? Bool ?? false
|
|
2533
|
+
previousDefaultChannel = hadPreviousDefaultChannel
|
|
2534
|
+
? UserDefaults.standard.string(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
2535
|
+
: nil
|
|
2536
|
+
do {
|
|
2537
|
+
try self.persistDefaultChannelPreviewSnapshot(
|
|
2538
|
+
channel: previousDefaultChannel,
|
|
2539
|
+
file: previewSnapshotFile
|
|
2540
|
+
)
|
|
2541
|
+
} catch {
|
|
2542
|
+
logger.warn("Cannot migrate default channel preview snapshot: \(error.localizedDescription)")
|
|
2543
|
+
return false
|
|
2544
|
+
}
|
|
2545
|
+
case .unreadable:
|
|
2546
|
+
logger.warn("Cannot restore default channel from an unreadable preview snapshot")
|
|
2547
|
+
return false
|
|
2548
|
+
}
|
|
2032
2549
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2550
|
+
return self.persistRestoredDefaultChannelFromPreview(
|
|
2551
|
+
channel: previousDefaultChannel,
|
|
2552
|
+
stateFile: stateFile,
|
|
2553
|
+
previewSnapshotFile: previewSnapshotFile
|
|
2554
|
+
)
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
private func applyRestoredDefaultChannelFromPreview(_ channel: String?) {
|
|
2558
|
+
let configDefaultChannel = self.getStringConfig("defaultChannel", defaultValue: "")
|
|
2559
|
+
if let channel = channel {
|
|
2560
|
+
UserDefaults.standard.set(channel, forKey: self.defaultChannelDefaultsKey)
|
|
2561
|
+
self.implementation.defaultChannel = channel
|
|
2562
|
+
logger.info("Restored defaultChannel after preview")
|
|
2563
|
+
} else {
|
|
2035
2564
|
UserDefaults.standard.removeObject(forKey: self.defaultChannelDefaultsKey)
|
|
2036
2565
|
self.implementation.defaultChannel = configDefaultChannel
|
|
2037
2566
|
logger.info("Restored defaultChannel after preview to config value")
|
|
2038
|
-
return
|
|
2039
2567
|
}
|
|
2568
|
+
}
|
|
2040
2569
|
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2570
|
+
private func persistRestoredDefaultChannelFromPreview(
|
|
2571
|
+
channel: String?,
|
|
2572
|
+
stateFile: URL,
|
|
2573
|
+
previewSnapshotFile: URL
|
|
2574
|
+
) -> Bool {
|
|
2575
|
+
defaultChannelStateLock.lock()
|
|
2576
|
+
defer { defaultChannelStateLock.unlock() }
|
|
2577
|
+
let didPersist: Bool
|
|
2578
|
+
do {
|
|
2579
|
+
try self.persistDefaultChannelStateWithoutLock(channel: channel, file: stateFile)
|
|
2580
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
2581
|
+
channel: nil,
|
|
2582
|
+
isValid: false,
|
|
2583
|
+
file: previewSnapshotFile
|
|
2584
|
+
)
|
|
2585
|
+
didPersist = true
|
|
2586
|
+
} catch {
|
|
2587
|
+
let persistedState = self.defaultChannelStateWithoutLock(file: stateFile)
|
|
2588
|
+
let persistedSnapshot = self.defaultChannelPreviewSnapshotWithoutLock(file: previewSnapshotFile)
|
|
2589
|
+
if persistedState.exists,
|
|
2590
|
+
persistedState.isReadable,
|
|
2591
|
+
persistedState.channel == channel,
|
|
2592
|
+
persistedSnapshot == .invalidated {
|
|
2593
|
+
logger.warn("Default channel preview restore committed before metadata update failed")
|
|
2594
|
+
didPersist = true
|
|
2595
|
+
} else {
|
|
2596
|
+
logger.warn("Cannot durably restore default channel after preview: \(error.localizedDescription)")
|
|
2597
|
+
didPersist = false
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
self.applyRestoredDefaultChannelFromPreview(channel)
|
|
2601
|
+
return didPersist
|
|
2044
2602
|
}
|
|
2045
2603
|
|
|
2046
2604
|
private func normalizedPreviewAppId(_ rawAppId: String?) -> String? {
|
|
@@ -2220,7 +2778,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2220
2778
|
self.shakeMenuGesture = Self.normalizedShakeMenuGesture(self.getStringConfig("shakeMenuGesture", defaultValue: Self.shakeMenuGestureShake))
|
|
2221
2779
|
self.syncShakeMenuGestureRecognizer()
|
|
2222
2780
|
self.restorePreviewPreviousAppId()
|
|
2223
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2781
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2782
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2783
|
+
}
|
|
2224
2784
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2225
2785
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
2226
2786
|
self.clearPreviewSessionPreferences()
|
|
@@ -2252,7 +2812,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2252
2812
|
guard self.previewSessionEnabled else {
|
|
2253
2813
|
return
|
|
2254
2814
|
}
|
|
2255
|
-
if let topVC = UIApplication.topViewController(),
|
|
2815
|
+
if let topVC = UIApplication.topViewController(self.bridge?.viewController),
|
|
2256
2816
|
topVC.isKind(of: UIAlertController.self) {
|
|
2257
2817
|
self.previewSessionAlertPending = true
|
|
2258
2818
|
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
@@ -2262,11 +2822,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2262
2822
|
|
|
2263
2823
|
let alert = UIAlertController(
|
|
2264
2824
|
title: "Preview started",
|
|
2265
|
-
message:
|
|
2825
|
+
message: self.shakeMenuGesture == Self.shakeMenuGestureThreeFingerPinch ? "Three-finger pinch to open menu." : "Shake to open menu.",
|
|
2266
2826
|
preferredStyle: .alert
|
|
2267
2827
|
)
|
|
2268
2828
|
alert.addAction(UIAlertAction(title: "Got it", style: .default))
|
|
2269
|
-
if let topVC = UIApplication.topViewController() {
|
|
2829
|
+
if let topVC = UIApplication.topViewController(self.bridge?.viewController) {
|
|
2270
2830
|
topVC.present(alert, animated: true)
|
|
2271
2831
|
} else {
|
|
2272
2832
|
self.previewSessionAlertPending = true
|
|
@@ -2465,6 +3025,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2465
3025
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
2466
3026
|
])
|
|
2467
3027
|
} else {
|
|
3028
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
3029
|
+
self.rejectCall(call, message: "Channel override removed but local persistence failed", code: "UNSETCHANNEL_PERSISTENCE_FAILED")
|
|
3030
|
+
return
|
|
3031
|
+
}
|
|
2468
3032
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
2469
3033
|
self.logger.info("Calling autoupdater after channel change!")
|
|
2470
3034
|
// Check if download is already in progress (with timeout protection)
|
|
@@ -2511,6 +3075,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2511
3075
|
"error": res.error.contains("Channel URL") ? "missing_config" : (res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed")) ? "channel_private" : "request_failed"
|
|
2512
3076
|
])
|
|
2513
3077
|
} else {
|
|
3078
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
3079
|
+
self.rejectCall(call, message: "Channel changed but local persistence failed", code: "SETCHANNEL_PERSISTENCE_FAILED")
|
|
3080
|
+
return
|
|
3081
|
+
}
|
|
2514
3082
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
2515
3083
|
self.logger.info("Calling autoupdater after channel change!")
|
|
2516
3084
|
// Check if download is already in progress (with timeout protection)
|
|
@@ -2535,6 +3103,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2535
3103
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
2536
3104
|
])
|
|
2537
3105
|
} else {
|
|
3106
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
3107
|
+
self.rejectCall(call, message: "Channel synchronized but local persistence failed", code: "GETCHANNEL_PERSISTENCE_FAILED")
|
|
3108
|
+
return
|
|
3109
|
+
}
|
|
2538
3110
|
self.resolveCall(call, data: res.toDict())
|
|
2539
3111
|
}
|
|
2540
3112
|
}
|
|
@@ -2674,10 +3246,67 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2674
3246
|
])
|
|
2675
3247
|
}
|
|
2676
3248
|
|
|
3249
|
+
private func reportAppLaunchStart() {
|
|
3250
|
+
guard !self.implementation.statsUrl.isEmpty, !launchStartReported else {
|
|
3251
|
+
return
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
launchStartReported = true
|
|
3255
|
+
let current = self.implementation.getCurrentBundle()
|
|
3256
|
+
self.implementation.sendStats(
|
|
3257
|
+
action: "app_launch_start",
|
|
3258
|
+
versionName: current.getVersionName(),
|
|
3259
|
+
oldVersionName: "",
|
|
3260
|
+
metadata: [
|
|
3261
|
+
"launch_started_at": String(launchStartedAtMs),
|
|
3262
|
+
"source": "plugin_load"
|
|
3263
|
+
]
|
|
3264
|
+
)
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
private func reportAppLaunchReady(_ bundle: BundleInfo) {
|
|
3268
|
+
guard !self.implementation.statsUrl.isEmpty, !launchReadyReported else {
|
|
3269
|
+
return
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
launchReadyReported = true
|
|
3273
|
+
let duration = max(0, Int64(Date().timeIntervalSince1970 * 1000) - launchStartedAtMs)
|
|
3274
|
+
self.implementation.sendStats(
|
|
3275
|
+
action: "app_launch_ready",
|
|
3276
|
+
versionName: bundle.getVersionName(),
|
|
3277
|
+
oldVersionName: "",
|
|
3278
|
+
metadata: [
|
|
3279
|
+
"duration_ms": String(duration),
|
|
3280
|
+
"launch_started_at": String(launchStartedAtMs),
|
|
3281
|
+
"source": "notify_app_ready"
|
|
3282
|
+
]
|
|
3283
|
+
)
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
private func reportAppLaunchTimeout(_ bundle: BundleInfo) {
|
|
3287
|
+
guard !self.implementation.statsUrl.isEmpty else {
|
|
3288
|
+
return
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
let duration = max(0, Int64(Date().timeIntervalSince1970 * 1000) - launchStartedAtMs)
|
|
3292
|
+
self.implementation.sendStats(
|
|
3293
|
+
action: "app_launch_timeout",
|
|
3294
|
+
versionName: bundle.getVersionName(),
|
|
3295
|
+
oldVersionName: "",
|
|
3296
|
+
metadata: [
|
|
3297
|
+
"duration_ms": String(duration),
|
|
3298
|
+
"launch_started_at": String(launchStartedAtMs),
|
|
3299
|
+
"timeout_ms": String(appReadyTimeout),
|
|
3300
|
+
"source": "app_ready_timeout"
|
|
3301
|
+
]
|
|
3302
|
+
)
|
|
3303
|
+
}
|
|
3304
|
+
|
|
2677
3305
|
@objc func notifyAppReady(_ call: CAPPluginCall) {
|
|
2678
3306
|
self.semaphoreDown()
|
|
2679
3307
|
let bundle = self.implementation.getCurrentBundle()
|
|
2680
3308
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
3309
|
+
self.reportAppLaunchReady(bundle)
|
|
2681
3310
|
logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
|
|
2682
3311
|
self.clearIncomingPreviewTransition()
|
|
2683
3312
|
self.hidePreviewTransitionLoader(reason: "notify-app-ready")
|
|
@@ -2785,16 +3414,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2785
3414
|
"bundle": current.toJSON()
|
|
2786
3415
|
])
|
|
2787
3416
|
self.persistLastFailedBundle(current)
|
|
3417
|
+
self.reportAppLaunchTimeout(current)
|
|
2788
3418
|
self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
|
|
2789
3419
|
self.implementation.setError(bundle: current)
|
|
2790
3420
|
_ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
|
|
2791
3421
|
if self.autoDeleteFailed && !current.isBuiltin() {
|
|
3422
|
+
let failedId = current.getId()
|
|
2792
3423
|
logger.info("Deleting failing bundle: \(current.toString())")
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3424
|
+
// Mark before async work so kill/OOM still resumes via drainPendingDeletes.
|
|
3425
|
+
self.implementation.saveBundleInfo(
|
|
3426
|
+
id: failedId,
|
|
3427
|
+
bundle: current.setStatus(status: BundleStatus.DELETING.storedValue)
|
|
3428
|
+
)
|
|
3429
|
+
UserDefaults.standard.synchronize()
|
|
3430
|
+
DispatchQueue.global(qos: .utility).async {
|
|
3431
|
+
let res = self.implementation.delete(id: failedId, removeInfo: false)
|
|
3432
|
+
if res {
|
|
3433
|
+
self.logger.info("Failed bundle deleted: \(failedId)")
|
|
3434
|
+
} else {
|
|
3435
|
+
self.logger.error("Failed to delete failed bundle: \(failedId)")
|
|
3436
|
+
}
|
|
2798
3437
|
}
|
|
2799
3438
|
}
|
|
2800
3439
|
} else {
|
|
@@ -2819,7 +3458,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2819
3458
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
2820
3459
|
logger.info("sendReadyToJs")
|
|
2821
3460
|
DispatchQueue.global().async {
|
|
2822
|
-
|
|
3461
|
+
// Only wait after load()/reload() armed the semaphore. Foreground resumes
|
|
3462
|
+
// with autoUpdate disabled call sendReadyToJs again without a fresh
|
|
3463
|
+
// notifyAppReady(), so waiting there always timed out after appReadyTimeout.
|
|
3464
|
+
if self.consumePendingNotifyAppReady() {
|
|
3465
|
+
self.semaphoreWait(waitTime: self.appReadyTimeout)
|
|
3466
|
+
}
|
|
2823
3467
|
self.notifyListeners("appReady", data: ["bundle": current.toJSON(), "status": msg], retainUntilConsumed: true)
|
|
2824
3468
|
|
|
2825
3469
|
// Auto hide splashscreen if enabled
|
|
@@ -2831,6 +3475,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2831
3475
|
}
|
|
2832
3476
|
}
|
|
2833
3477
|
|
|
3478
|
+
private func consumePendingNotifyAppReady() -> Bool {
|
|
3479
|
+
let shouldWait = pendingNotifyAppReady
|
|
3480
|
+
pendingNotifyAppReady = false
|
|
3481
|
+
return shouldWait
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
private func clearPendingNotifyAppReady() {
|
|
3485
|
+
pendingNotifyAppReady = false
|
|
3486
|
+
}
|
|
3487
|
+
|
|
2834
3488
|
private func hideSplashscreen() {
|
|
2835
3489
|
if Thread.isMainThread {
|
|
2836
3490
|
self.performHideSplashscreen()
|
|
@@ -3382,6 +4036,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3382
4036
|
isAutoUpdateModeEnabled(autoUpdateMode) && autoUpdateMode != autoUpdateModeOnlyDownload
|
|
3383
4037
|
}
|
|
3384
4038
|
|
|
4039
|
+
/// Incomplete leftovers must be downloaded again. Android already skips
|
|
4040
|
+
/// `DOWNLOADING` here; iOS used to treat them as ready, then `setNextBundle`
|
|
4041
|
+
/// failed quietly and the update never applied.
|
|
4042
|
+
static func shouldRetryDownloadForExistingBundle(_ bundle: BundleInfo) -> Bool {
|
|
4043
|
+
bundle.isDeleted() || bundle.isDeleting() || bundle.isDownloading()
|
|
4044
|
+
}
|
|
4045
|
+
|
|
3385
4046
|
static func isDirectUpdateMode(_ directUpdateMode: String) -> Bool {
|
|
3386
4047
|
directUpdateMode == autoUpdateModeInstall || directUpdateMode == autoUpdateModeLaunch || directUpdateMode == autoUpdateModeAlways
|
|
3387
4048
|
}
|
|
@@ -3444,6 +4105,34 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3444
4105
|
self.currentBuildVersion = currentBuildVersion
|
|
3445
4106
|
}
|
|
3446
4107
|
|
|
4108
|
+
func setAppReadyTimeoutForTesting(_ timeout: Int) {
|
|
4109
|
+
self.appReadyTimeout = timeout
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
func armPendingNotifyAppReadyForTesting() {
|
|
4113
|
+
pendingNotifyAppReady = true
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
func resetSemaphoreWaitTestingStateForTesting() {
|
|
4117
|
+
semaphoreWaitTestingLock.lock()
|
|
4118
|
+
didEnterSemaphoreWaitForTesting = false
|
|
4119
|
+
semaphoreWaitTestingLock.unlock()
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
var didEnterSemaphoreWaitForTestingState: Bool {
|
|
4123
|
+
semaphoreWaitTestingLock.lock()
|
|
4124
|
+
defer { semaphoreWaitTestingLock.unlock() }
|
|
4125
|
+
return didEnterSemaphoreWaitForTesting
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
func clearPendingNotifyAppReadyForTesting() {
|
|
4129
|
+
clearPendingNotifyAppReady()
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
var isPendingNotifyAppReadyForTesting: Bool {
|
|
4133
|
+
pendingNotifyAppReady
|
|
4134
|
+
}
|
|
4135
|
+
|
|
3447
4136
|
func shouldUseDirectUpdateForTesting() -> Bool {
|
|
3448
4137
|
self.shouldUseDirectUpdate()
|
|
3449
4138
|
}
|
|
@@ -3593,7 +4282,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3593
4282
|
func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
|
|
3594
4283
|
// Live update checks/downloads are user-visible work. Using `.background`
|
|
3595
4284
|
// lets the scheduler starve them for minutes while the app is active.
|
|
3596
|
-
DispatchQueue.global(qos: .
|
|
4285
|
+
DispatchQueue.global(qos: .userInitiated).async(execute: work)
|
|
3597
4286
|
}
|
|
3598
4287
|
|
|
3599
4288
|
private func beginDownloadBackgroundTask() {
|
|
@@ -3644,8 +4333,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3644
4333
|
}
|
|
3645
4334
|
|
|
3646
4335
|
self.runBackgroundDownloadWork {
|
|
3647
|
-
// Wait for cleanup to complete before starting download
|
|
3648
|
-
self.waitForCleanupIfNeeded()
|
|
3649
4336
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3650
4337
|
self.clearDownloadInProgressState()
|
|
3651
4338
|
return
|
|
@@ -3653,7 +4340,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3653
4340
|
self.beginDownloadBackgroundTask()
|
|
3654
4341
|
self.logger.info("Check for update via \(self.updateUrl)")
|
|
3655
4342
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
3656
|
-
|
|
4343
|
+
var current = self.implementation.getCurrentBundle()
|
|
3657
4344
|
if self.shouldBlockAutoUpdateForPreviewSession() {
|
|
3658
4345
|
self.clearDownloadInProgressState()
|
|
3659
4346
|
self.endBackGroundTask()
|
|
@@ -3672,6 +4359,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3672
4359
|
)
|
|
3673
4360
|
return
|
|
3674
4361
|
}
|
|
4362
|
+
// File mutations wait here. getLatest already ran in parallel with cleanup.
|
|
4363
|
+
do {
|
|
4364
|
+
try self.waitForCleanupIfNeeded()
|
|
4365
|
+
} catch {
|
|
4366
|
+
self.logger.error("Cleanup still running, skipping download")
|
|
4367
|
+
self.clearDownloadInProgressState()
|
|
4368
|
+
self.endBackGroundTask()
|
|
4369
|
+
return
|
|
4370
|
+
}
|
|
4371
|
+
current = self.implementation.getCurrentBundle()
|
|
3675
4372
|
if res.version == "builtin" {
|
|
3676
4373
|
self.logger.info("Latest version is builtin")
|
|
3677
4374
|
let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
|
|
@@ -3733,14 +4430,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3733
4430
|
do {
|
|
3734
4431
|
self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
|
|
3735
4432
|
var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
4433
|
+
let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
|
|
4434
|
+
if needsDownload {
|
|
4435
|
+
if let existing = nextImpl {
|
|
4436
|
+
self.logger.info("Latest bundle already exists in incomplete state (\(existing.getStatus())) and will be deleted, download will overwrite it.")
|
|
4437
|
+
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
4438
|
+
let deleted = self.implementation.delete(id: existing.getId(), removeInfo: true)
|
|
4439
|
+
if deleted {
|
|
4440
|
+
self.logger.info("Incomplete bundle deleted: \(existing.toString())")
|
|
3742
4441
|
} else {
|
|
3743
|
-
self.logger.error("Failed to delete
|
|
4442
|
+
self.logger.error("Failed to delete incomplete bundle: \(existing.toString())")
|
|
3744
4443
|
}
|
|
3745
4444
|
}
|
|
3746
4445
|
self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
|
|
@@ -3827,8 +4526,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3827
4526
|
error: false,
|
|
3828
4527
|
plannedDirectUpdate: plannedDirectUpdate
|
|
3829
4528
|
)
|
|
3830
|
-
} else {
|
|
3831
|
-
_ = self.implementation.setNextBundle(next: next.getId())
|
|
4529
|
+
} else if self.queueBundleForNextBackgroundInstall(next) {
|
|
3832
4530
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
3833
4531
|
self.endBackGroundTaskWithNotif(
|
|
3834
4532
|
msg: "Direct update reload failed, update will install next background",
|
|
@@ -3837,20 +4535,35 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3837
4535
|
error: false,
|
|
3838
4536
|
plannedDirectUpdate: plannedDirectUpdate
|
|
3839
4537
|
)
|
|
4538
|
+
} else {
|
|
4539
|
+
self.endBackGroundTaskWithNotif(
|
|
4540
|
+
msg: "Direct update reload failed, and next bundle could not be queued",
|
|
4541
|
+
latestVersionName: latestVersionName,
|
|
4542
|
+
current: current,
|
|
4543
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4544
|
+
)
|
|
3840
4545
|
}
|
|
3841
4546
|
} else if self.shouldAutoSetNextBundle() {
|
|
3842
4547
|
if plannedDirectUpdate && !directUpdateAllowed {
|
|
3843
4548
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
3844
4549
|
}
|
|
3845
|
-
self.
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4550
|
+
if self.queueBundleForNextBackgroundInstall(next) {
|
|
4551
|
+
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4552
|
+
self.endBackGroundTaskWithNotif(
|
|
4553
|
+
msg: "update downloaded, will install next background",
|
|
4554
|
+
latestVersionName: latestVersionName,
|
|
4555
|
+
current: current,
|
|
4556
|
+
error: false,
|
|
4557
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4558
|
+
)
|
|
4559
|
+
} else {
|
|
4560
|
+
self.endBackGroundTaskWithNotif(
|
|
4561
|
+
msg: "Update downloaded, but next bundle could not be queued",
|
|
4562
|
+
latestVersionName: latestVersionName,
|
|
4563
|
+
current: current,
|
|
4564
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
4565
|
+
)
|
|
4566
|
+
}
|
|
3854
4567
|
} else {
|
|
3855
4568
|
self.logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle")
|
|
3856
4569
|
self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()], retainUntilConsumed: true)
|
|
@@ -4014,6 +4727,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4014
4727
|
|
|
4015
4728
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
4016
4729
|
self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
|
|
4730
|
+
self.implementation.persistPendingStats()
|
|
4017
4731
|
logger.info("Check for pending update")
|
|
4018
4732
|
|
|
4019
4733
|
// Show splashscreen only if autoSplashscreen is enabled AND autoUpdate is enabled AND directUpdate would be used
|