@capgo/capacitor-updater 8.51.0 → 8.51.2
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/README.md +50 -47
- 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 +251 -85
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +289 -60
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +1 -1
- package/dist/docs.json +26 -2
- package/dist/esm/definitions.d.ts +23 -7
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +5 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +609 -69
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +187 -34
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +8 -0
- package/package.json +1 -1
|
@@ -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,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
85
92
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
86
93
|
]
|
|
87
94
|
public var implementation = CapgoUpdater()
|
|
88
|
-
private let pluginVersion: String = "8.51.
|
|
95
|
+
private let pluginVersion: String = "8.51.2"
|
|
89
96
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
90
97
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
91
98
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -124,6 +131,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
124
131
|
private let previewSourceDefaultsKey = "CapacitorUpdater.previewSource"
|
|
125
132
|
private let previewSessionsDefaultsKey = "CapacitorUpdater.previewSessions"
|
|
126
133
|
private let previewSessionAlertPendingDefaultsKey = "CapacitorUpdater.previewSessionAlertPending"
|
|
134
|
+
private let defaultChannelInstallMarkerDefaultsKey = "CapacitorUpdater.defaultChannelInstallMarkerCreated"
|
|
135
|
+
private let defaultChannelInstallMarkerFilename = "CapacitorUpdater.defaultChannelInstallMarker"
|
|
136
|
+
private let defaultChannelStateFilename = "CapacitorUpdater.defaultChannelState"
|
|
137
|
+
private let defaultChannelPreviewSnapshotFilename = "CapacitorUpdater.defaultChannelPreviewSnapshot"
|
|
127
138
|
private let previewDeepLinkScheme = "capgo"
|
|
128
139
|
private let previewDeepLinkRootComponent = "preview"
|
|
129
140
|
private let previewDeepLinkChannelComponent = "channel"
|
|
@@ -163,6 +174,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
163
174
|
private var autoDeleteFailed = false
|
|
164
175
|
private var autoDeletePrevious = false
|
|
165
176
|
var allowSetDefaultChannel = true
|
|
177
|
+
var persistDefaultChannelOnReinstall = true
|
|
166
178
|
private var keepUrlPathAfterReload = false
|
|
167
179
|
private var backgroundWork: DispatchWorkItem?
|
|
168
180
|
private var taskRunning = false
|
|
@@ -175,8 +187,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
175
187
|
|
|
176
188
|
// Lock to ensure cleanup completes before downloads start
|
|
177
189
|
private let cleanupLock = NSLock()
|
|
190
|
+
private let defaultChannelStateLock = NSLock()
|
|
191
|
+
private let cleanupGroup = DispatchGroup()
|
|
178
192
|
private var cleanupComplete = false
|
|
179
193
|
private var cleanupThread: Thread?
|
|
194
|
+
private var defaultChannelCleanupMustRetry = false
|
|
180
195
|
private var persistCustomId = false
|
|
181
196
|
private var persistModifyUrl = false
|
|
182
197
|
private var allowManualBundleError = false
|
|
@@ -194,6 +209,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
194
209
|
private var isLeavingPreviewForIncomingLink = false
|
|
195
210
|
private var previewTransitionClearWorkItem: DispatchWorkItem?
|
|
196
211
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
212
|
+
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
213
|
+
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
214
|
+
private var pendingNotifyAppReady = false
|
|
197
215
|
|
|
198
216
|
private var delayUpdateUtils: DelayUpdateUtils!
|
|
199
217
|
|
|
@@ -219,6 +237,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
219
237
|
self.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId()
|
|
220
238
|
persistCustomId = getConfig().getBoolean("persistCustomId", false)
|
|
221
239
|
allowSetDefaultChannel = getConfig().getBoolean("allowSetDefaultChannel", true)
|
|
240
|
+
persistDefaultChannelOnReinstall = getConfig().getBoolean("persistDefaultChannelOnReinstall", true)
|
|
222
241
|
if persistCustomId {
|
|
223
242
|
let storedCustomId = UserDefaults.standard.string(forKey: customIdDefaultsKey) ?? ""
|
|
224
243
|
if !storedCustomId.isEmpty {
|
|
@@ -335,12 +354,57 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
335
354
|
}
|
|
336
355
|
}
|
|
337
356
|
|
|
338
|
-
|
|
339
|
-
|
|
357
|
+
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
358
|
+
let defaultChannelPersistenceDisabled = !persistDefaultChannelOnReinstall
|
|
359
|
+
let restoredReinstall = defaultChannelPersistenceDisabled && self.isRestoredReinstall()
|
|
360
|
+
var installMarkerCanBePrepared = true
|
|
361
|
+
if shouldClearPersistedDefaultChannel(
|
|
362
|
+
nativeBuildVersionChanged: nativeBuildVersionChanged,
|
|
363
|
+
resetWhenUpdate: resetWhenUpdate,
|
|
364
|
+
restoredReinstall: restoredReinstall
|
|
365
|
+
) {
|
|
366
|
+
installMarkerCanBePrepared = clearPersistedDefaultChannel()
|
|
367
|
+
if installMarkerCanBePrepared {
|
|
368
|
+
logger.info("Cleared persisted defaultChannel because reinstall persistence is disabled")
|
|
369
|
+
} else {
|
|
370
|
+
logger.warn("Cannot durably clear persisted defaultChannel")
|
|
371
|
+
self.defaultChannelCleanupMustRetry = true
|
|
372
|
+
self.invalidateDefaultChannelInstallMarker()
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if defaultChannelPersistenceDisabled && installMarkerCanBePrepared {
|
|
376
|
+
self.prepareDefaultChannelInstallMarker()
|
|
377
|
+
}
|
|
378
|
+
if !previewSessionEnabled,
|
|
379
|
+
!defaultChannelCleanupMustRetry,
|
|
380
|
+
self.hasPendingDefaultChannelPreviewSnapshot(),
|
|
381
|
+
!self.restorePreviewPreviousDefaultChannel() {
|
|
382
|
+
logger.warn("Default channel preview restore remains pending")
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let configDefaultChannel = getConfig().getString("defaultChannel", "")!
|
|
386
|
+
let storedDefaultChannel = UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
387
|
+
let stateFile = self.defaultChannelStateFile()
|
|
388
|
+
let state = stateFile.map { self.defaultChannelState(file: $0) }
|
|
389
|
+
if defaultChannelCleanupMustRetry {
|
|
390
|
+
implementation.defaultChannel = configDefaultChannel
|
|
391
|
+
logger.warn("Using configured defaultChannel until persisted cleanup can retry")
|
|
392
|
+
} else if let state = state, state.exists, state.isReadable {
|
|
393
|
+
self.reconcileDefaultChannelDefaults(with: state.channel)
|
|
394
|
+
implementation.defaultChannel = state.channel ?? configDefaultChannel
|
|
395
|
+
logger.info("Loaded persisted defaultChannel from local state")
|
|
396
|
+
} else if defaultChannelPersistenceDisabled, state?.exists == true {
|
|
397
|
+
implementation.defaultChannel = configDefaultChannel
|
|
398
|
+
logger.warn("Ignoring unreadable persisted defaultChannel while reinstall persistence is disabled")
|
|
399
|
+
} else if let storedDefaultChannel = storedDefaultChannel {
|
|
340
400
|
implementation.defaultChannel = storedDefaultChannel
|
|
341
401
|
logger.info("Loaded persisted defaultChannel from setChannel()")
|
|
342
402
|
} else {
|
|
343
|
-
implementation.defaultChannel =
|
|
403
|
+
implementation.defaultChannel = configDefaultChannel
|
|
404
|
+
}
|
|
405
|
+
if !defaultChannelCleanupMustRetry,
|
|
406
|
+
state?.exists != true || (!defaultChannelPersistenceDisabled && state?.isReadable != true) {
|
|
407
|
+
_ = self.persistDefaultChannelStateFromDefaults()
|
|
344
408
|
}
|
|
345
409
|
self.reportAppLaunchStart()
|
|
346
410
|
self.implementation.autoReset()
|
|
@@ -351,16 +415,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
351
415
|
|
|
352
416
|
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
353
417
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
354
|
-
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
355
418
|
if nativeBuildVersionChanged {
|
|
356
419
|
self.clearPreviewSessionForNativeBuildChange()
|
|
357
420
|
}
|
|
358
421
|
self.leavePreviewSessionForLaunchURLIfNeeded()
|
|
359
422
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
self
|
|
423
|
+
// Downloads (including shake-menu / CapgoUpdater entry points) wait on this gate.
|
|
424
|
+
self.implementation.beforeDownload = { [weak self] in
|
|
425
|
+
self?.waitForCleanupIfNeeded()
|
|
363
426
|
}
|
|
427
|
+
// Always run async cleanup: delete obsolete bundles on native update (when enabled)
|
|
428
|
+
// and sweep orphan directories every launch. Must not block app startup.
|
|
429
|
+
if !resetWhenUpdate {
|
|
430
|
+
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
431
|
+
UserDefaults.standard.synchronize()
|
|
432
|
+
}
|
|
433
|
+
let didResetCurrentBundle = resetWhenUpdate ? self.resetCurrentBundleForNativeBuildChangeIfNeeded() : false
|
|
434
|
+
self.cleanupObsoleteVersions(
|
|
435
|
+
resetWhenUpdate: resetWhenUpdate,
|
|
436
|
+
didResetCurrentBundle: didResetCurrentBundle
|
|
437
|
+
)
|
|
364
438
|
self.reportNativeVersionStatsIfChanged()
|
|
365
439
|
|
|
366
440
|
// Load the server
|
|
@@ -522,6 +596,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
522
596
|
}
|
|
523
597
|
|
|
524
598
|
private func semaphoreUp() {
|
|
599
|
+
pendingNotifyAppReady = true
|
|
525
600
|
DispatchQueue.global().async {
|
|
526
601
|
self.semaphoreWait(waitTime: 0)
|
|
527
602
|
}
|
|
@@ -535,6 +610,327 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
535
610
|
UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
536
611
|
}
|
|
537
612
|
|
|
613
|
+
func shouldClearPersistedDefaultChannel(
|
|
614
|
+
nativeBuildVersionChanged: Bool,
|
|
615
|
+
resetWhenUpdate: Bool,
|
|
616
|
+
restoredReinstall: Bool
|
|
617
|
+
) -> Bool {
|
|
618
|
+
!persistDefaultChannelOnReinstall && (restoredReinstall || (resetWhenUpdate && nativeBuildVersionChanged))
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
func clearPersistedDefaultChannel() -> Bool {
|
|
622
|
+
guard let stateFile = self.defaultChannelStateFile(),
|
|
623
|
+
let previewSnapshotFile = self.defaultChannelPreviewSnapshotFile() else {
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
return self.clearPersistedDefaultChannel(
|
|
627
|
+
stateFile: stateFile,
|
|
628
|
+
previewSnapshotFile: previewSnapshotFile
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
func clearPersistedDefaultChannel(stateFile: URL, previewSnapshotFile: URL) -> Bool {
|
|
633
|
+
defaultChannelStateLock.lock()
|
|
634
|
+
defer { defaultChannelStateLock.unlock() }
|
|
635
|
+
do {
|
|
636
|
+
try self.persistDefaultChannelStateWithoutLock(channel: nil, file: stateFile)
|
|
637
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
638
|
+
channel: nil,
|
|
639
|
+
isValid: false,
|
|
640
|
+
file: previewSnapshotFile
|
|
641
|
+
)
|
|
642
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelDefaultsKey)
|
|
643
|
+
UserDefaults.standard.removeObject(forKey: previewPreviousDefaultChannelDefaultsKey)
|
|
644
|
+
UserDefaults.standard.removeObject(forKey: previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
645
|
+
return true
|
|
646
|
+
} catch {
|
|
647
|
+
logger.warn("Cannot persist cleared default channel state: \(error.localizedDescription)")
|
|
648
|
+
return false
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private func defaultChannelStateFile() -> URL? {
|
|
653
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
654
|
+
.appendingPathComponent(defaultChannelStateFilename)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private func defaultChannelPreviewSnapshotFile() -> URL? {
|
|
658
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
659
|
+
.appendingPathComponent(defaultChannelPreviewSnapshotFilename)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
func defaultChannelState(file: URL) -> (exists: Bool, channel: String?, isReadable: Bool) {
|
|
663
|
+
defaultChannelStateLock.lock()
|
|
664
|
+
defer { defaultChannelStateLock.unlock() }
|
|
665
|
+
return self.defaultChannelStateWithoutLock(file: file)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private func defaultChannelStateWithoutLock(file: URL) -> (exists: Bool, channel: String?, isReadable: Bool) {
|
|
669
|
+
let fileManager = FileManager.default
|
|
670
|
+
guard fileManager.fileExists(atPath: file.path) else {
|
|
671
|
+
return (false, nil, true)
|
|
672
|
+
}
|
|
673
|
+
do {
|
|
674
|
+
let data = try Data(contentsOf: file)
|
|
675
|
+
guard !data.isEmpty else {
|
|
676
|
+
return (true, nil, true)
|
|
677
|
+
}
|
|
678
|
+
guard let channel = String(data: data, encoding: .utf8) else {
|
|
679
|
+
logger.warn("Cannot decode persisted default channel state")
|
|
680
|
+
return (true, nil, false)
|
|
681
|
+
}
|
|
682
|
+
return (true, channel, true)
|
|
683
|
+
} catch {
|
|
684
|
+
logger.warn("Cannot read persisted default channel state: \(error.localizedDescription)")
|
|
685
|
+
return (true, nil, false)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
func persistDefaultChannelState(channel: String?, file: URL) throws {
|
|
690
|
+
defaultChannelStateLock.lock()
|
|
691
|
+
defer { defaultChannelStateLock.unlock() }
|
|
692
|
+
try self.persistDefaultChannelStateWithoutLock(channel: channel, file: file)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private func persistDefaultChannelStateWithoutLock(channel: String?, file: URL) throws {
|
|
696
|
+
let data = channel.map { Data($0.utf8) } ?? Data()
|
|
697
|
+
try self.persistBackupExcludedFile(file: file, data: data, overwrite: true)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
func defaultChannelPreviewSnapshot(file: URL) -> DefaultChannelPreviewSnapshot {
|
|
701
|
+
defaultChannelStateLock.lock()
|
|
702
|
+
defer { defaultChannelStateLock.unlock() }
|
|
703
|
+
return self.defaultChannelPreviewSnapshotWithoutLock(file: file)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private func defaultChannelPreviewSnapshotWithoutLock(file: URL) -> DefaultChannelPreviewSnapshot {
|
|
707
|
+
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
708
|
+
return .missing
|
|
709
|
+
}
|
|
710
|
+
do {
|
|
711
|
+
let data = try Data(contentsOf: file)
|
|
712
|
+
guard let kind = data.first else {
|
|
713
|
+
return .unreadable
|
|
714
|
+
}
|
|
715
|
+
switch kind {
|
|
716
|
+
case 0 where data.count == 1:
|
|
717
|
+
return .invalidated
|
|
718
|
+
case 1 where data.count == 1:
|
|
719
|
+
return .snapshot(nil)
|
|
720
|
+
case 2:
|
|
721
|
+
guard let channel = String(data: Data(data.dropFirst()), encoding: .utf8) else {
|
|
722
|
+
return .unreadable
|
|
723
|
+
}
|
|
724
|
+
return .snapshot(channel)
|
|
725
|
+
default:
|
|
726
|
+
return .unreadable
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
logger.warn("Cannot read default channel preview snapshot: \(error.localizedDescription)")
|
|
730
|
+
return .unreadable
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
func persistDefaultChannelPreviewSnapshot(channel: String?, file: URL) throws {
|
|
735
|
+
defaultChannelStateLock.lock()
|
|
736
|
+
defer { defaultChannelStateLock.unlock() }
|
|
737
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
738
|
+
channel: channel,
|
|
739
|
+
isValid: true,
|
|
740
|
+
file: file
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
func invalidateDefaultChannelPreviewSnapshot(file: URL) throws {
|
|
745
|
+
defaultChannelStateLock.lock()
|
|
746
|
+
defer { defaultChannelStateLock.unlock() }
|
|
747
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
748
|
+
channel: nil,
|
|
749
|
+
isValid: false,
|
|
750
|
+
file: file
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private func persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
755
|
+
channel: String?,
|
|
756
|
+
isValid: Bool,
|
|
757
|
+
file: URL
|
|
758
|
+
) throws {
|
|
759
|
+
var data = Data()
|
|
760
|
+
if !isValid {
|
|
761
|
+
data.append(0)
|
|
762
|
+
} else if let channel = channel {
|
|
763
|
+
data.append(2)
|
|
764
|
+
data.append(contentsOf: channel.utf8)
|
|
765
|
+
} else {
|
|
766
|
+
data.append(1)
|
|
767
|
+
}
|
|
768
|
+
try self.persistBackupExcludedFile(file: file, data: data, overwrite: true)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private func persistDefaultChannelPreviewSnapshot(channel: String?) -> Bool {
|
|
772
|
+
guard let file = self.defaultChannelPreviewSnapshotFile() else {
|
|
773
|
+
return false
|
|
774
|
+
}
|
|
775
|
+
do {
|
|
776
|
+
try self.persistDefaultChannelPreviewSnapshot(channel: channel, file: file)
|
|
777
|
+
return true
|
|
778
|
+
} catch {
|
|
779
|
+
logger.warn("Cannot persist default channel preview snapshot: \(error.localizedDescription)")
|
|
780
|
+
return false
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private func hasPendingDefaultChannelPreviewSnapshot() -> Bool {
|
|
785
|
+
guard let file = self.defaultChannelPreviewSnapshotFile() else {
|
|
786
|
+
return false
|
|
787
|
+
}
|
|
788
|
+
switch self.defaultChannelPreviewSnapshot(file: file) {
|
|
789
|
+
case .snapshot, .unreadable:
|
|
790
|
+
return true
|
|
791
|
+
case .missing, .invalidated:
|
|
792
|
+
return false
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
@discardableResult
|
|
797
|
+
func persistDefaultChannelStateFromDefaults() -> Bool {
|
|
798
|
+
guard let file = self.defaultChannelStateFile() else {
|
|
799
|
+
logger.warn("Cannot locate default channel state file")
|
|
800
|
+
return false
|
|
801
|
+
}
|
|
802
|
+
return self.persistDefaultChannelStateFromDefaults(stateFile: file)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
@discardableResult
|
|
806
|
+
func persistDefaultChannelStateFromDefaults(stateFile: URL) -> Bool {
|
|
807
|
+
defaultChannelStateLock.lock()
|
|
808
|
+
defer { defaultChannelStateLock.unlock() }
|
|
809
|
+
do {
|
|
810
|
+
try self.persistDefaultChannelStateWithoutLock(
|
|
811
|
+
channel: UserDefaults.standard.string(forKey: defaultChannelDefaultsKey),
|
|
812
|
+
file: stateFile
|
|
813
|
+
)
|
|
814
|
+
return true
|
|
815
|
+
} catch {
|
|
816
|
+
do {
|
|
817
|
+
if FileManager.default.fileExists(atPath: stateFile.path) {
|
|
818
|
+
try FileManager.default.removeItem(at: stateFile)
|
|
819
|
+
}
|
|
820
|
+
logger.warn("Cannot persist default channel state; falling back to UserDefaults: \(error.localizedDescription)")
|
|
821
|
+
return true
|
|
822
|
+
} catch let invalidationError {
|
|
823
|
+
logger.warn("Cannot persist or invalidate default channel state: \(invalidationError.localizedDescription)")
|
|
824
|
+
return false
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private func persistedDefaultChannel() -> String? {
|
|
830
|
+
guard let file = self.defaultChannelStateFile() else {
|
|
831
|
+
return UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
832
|
+
}
|
|
833
|
+
return self.persistedDefaultChannel(stateFile: file)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
func persistedDefaultChannel(stateFile: URL) -> String? {
|
|
837
|
+
let state = self.defaultChannelState(file: stateFile)
|
|
838
|
+
if state.exists {
|
|
839
|
+
if state.isReadable {
|
|
840
|
+
return state.channel
|
|
841
|
+
}
|
|
842
|
+
if !persistDefaultChannelOnReinstall {
|
|
843
|
+
return nil
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private func reconcileDefaultChannelDefaults(with channel: String?) {
|
|
850
|
+
if let channel = channel {
|
|
851
|
+
UserDefaults.standard.set(channel, forKey: defaultChannelDefaultsKey)
|
|
852
|
+
} else {
|
|
853
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelDefaultsKey)
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private func defaultChannelInstallMarker() -> URL? {
|
|
858
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
859
|
+
.appendingPathComponent(defaultChannelInstallMarkerFilename)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private func invalidateDefaultChannelInstallMarker() {
|
|
863
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
864
|
+
return
|
|
865
|
+
}
|
|
866
|
+
do {
|
|
867
|
+
try self.invalidateDefaultChannelInstallMarker(marker: marker)
|
|
868
|
+
} catch {
|
|
869
|
+
logger.warn("Cannot invalidate default channel install marker for cleanup retry: \(error.localizedDescription)")
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
func invalidateDefaultChannelInstallMarker(marker: URL) throws {
|
|
874
|
+
let fileManager = FileManager.default
|
|
875
|
+
if fileManager.fileExists(atPath: marker.path) {
|
|
876
|
+
try fileManager.removeItem(at: marker)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
func isRestoredReinstall() -> Bool {
|
|
881
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
882
|
+
return false
|
|
883
|
+
}
|
|
884
|
+
return self.isRestoredReinstall(
|
|
885
|
+
marker: marker,
|
|
886
|
+
markerWasCreated: UserDefaults.standard.bool(forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
func isRestoredReinstall(marker: URL, markerWasCreated: Bool) -> Bool {
|
|
891
|
+
markerWasCreated && !FileManager.default.fileExists(atPath: marker.path)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private func prepareDefaultChannelInstallMarker() {
|
|
895
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
self.prepareDefaultChannelInstallMarker(
|
|
899
|
+
marker: marker,
|
|
900
|
+
markerWasCreated: UserDefaults.standard.bool(forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
func prepareDefaultChannelInstallMarker(marker: URL, markerWasCreated: Bool) {
|
|
905
|
+
do {
|
|
906
|
+
try self.persistBackupExcludedFile(file: marker, data: Data())
|
|
907
|
+
if !markerWasCreated {
|
|
908
|
+
UserDefaults.standard.set(true, forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
909
|
+
UserDefaults.standard.synchronize()
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
try? FileManager.default.removeItem(at: marker)
|
|
913
|
+
UserDefaults.standard.set(false, forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
914
|
+
UserDefaults.standard.synchronize()
|
|
915
|
+
logger.warn("Cannot prepare default channel install marker: \(error.localizedDescription)")
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private func persistBackupExcludedFile(file: URL, data: Data, overwrite: Bool = false) throws {
|
|
920
|
+
let fileManager = FileManager.default
|
|
921
|
+
if overwrite || !fileManager.fileExists(atPath: file.path) {
|
|
922
|
+
try fileManager.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
923
|
+
try data.write(to: file, options: .atomic)
|
|
924
|
+
}
|
|
925
|
+
let currentResourceValues = try file.resourceValues(forKeys: [.isExcludedFromBackupKey])
|
|
926
|
+
if currentResourceValues.isExcludedFromBackup != true {
|
|
927
|
+
var resourceValues = URLResourceValues()
|
|
928
|
+
resourceValues.isExcludedFromBackup = true
|
|
929
|
+
var mutableFile = file
|
|
930
|
+
try mutableFile.setResourceValues(resourceValues)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
538
934
|
func hasNativeBuildVersionChanged() -> Bool {
|
|
539
935
|
let previous = self.storedNativeBuildVersion()
|
|
540
936
|
return previous != "0" && self.currentBuildVersion != previous
|
|
@@ -633,12 +1029,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
633
1029
|
return true
|
|
634
1030
|
}
|
|
635
1031
|
|
|
636
|
-
private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
|
|
1032
|
+
private func cleanupObsoleteVersions(resetWhenUpdate: Bool = true, didResetCurrentBundle: Bool = false) {
|
|
1033
|
+
// Enter before start so waiters never race past an unstarted cleanup thread.
|
|
1034
|
+
self.cleanupComplete = false
|
|
1035
|
+
self.cleanupGroup.enter()
|
|
637
1036
|
cleanupThread = Thread {
|
|
1037
|
+
let bgTaskLock = NSLock()
|
|
1038
|
+
var cleanupBackgroundTask = UIBackgroundTaskIdentifier.invalid
|
|
1039
|
+
let endCleanupBackgroundTask = {
|
|
1040
|
+
bgTaskLock.lock()
|
|
1041
|
+
defer { bgTaskLock.unlock() }
|
|
1042
|
+
if cleanupBackgroundTask != .invalid {
|
|
1043
|
+
UIApplication.shared.endBackgroundTask(cleanupBackgroundTask)
|
|
1044
|
+
cleanupBackgroundTask = .invalid
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
cleanupBackgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CapgoBundleCleanup") {
|
|
1048
|
+
endCleanupBackgroundTask()
|
|
1049
|
+
}
|
|
638
1050
|
self.cleanupLock.lock()
|
|
639
1051
|
defer {
|
|
640
1052
|
self.cleanupComplete = true
|
|
641
1053
|
self.cleanupLock.unlock()
|
|
1054
|
+
self.cleanupGroup.leave()
|
|
1055
|
+
endCleanupBackgroundTask()
|
|
642
1056
|
self.logger.info("Cleanup complete")
|
|
643
1057
|
}
|
|
644
1058
|
|
|
@@ -669,54 +1083,41 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
669
1083
|
// 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
|
|
670
1084
|
|
|
671
1085
|
let previous = self.storedNativeBuildVersion()
|
|
672
|
-
|
|
1086
|
+
let nativeVersionChanged = previous != "0" && self.currentBuildVersion != previous
|
|
1087
|
+
if resetWhenUpdate && nativeVersionChanged {
|
|
673
1088
|
if !didResetCurrentBundle {
|
|
674
1089
|
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
|
|
675
1090
|
self.implementation.reset(isInternal: true)
|
|
676
1091
|
}
|
|
677
1092
|
let res = self.implementation.list()
|
|
678
1093
|
for version in res {
|
|
679
|
-
// Check if thread was cancelled
|
|
680
|
-
if Thread.current.isCancelled {
|
|
681
|
-
self.logger.warn("Cleanup was cancelled, stopping")
|
|
682
|
-
return
|
|
683
|
-
}
|
|
684
1094
|
self.logger.info("Deleting obsolete bundle: \(version.getId())")
|
|
685
|
-
let
|
|
686
|
-
if !
|
|
1095
|
+
let deleted = self.implementation.delete(id: version.getId())
|
|
1096
|
+
if !deleted {
|
|
687
1097
|
self.logger.error("Delete failed, id \(version.getId()) doesn't exist")
|
|
688
1098
|
}
|
|
1099
|
+
Thread.sleep(forTimeInterval: 0.075)
|
|
689
1100
|
}
|
|
690
|
-
|
|
691
|
-
let storedBundles = self.implementation.list(raw: true)
|
|
692
|
-
let allowedIds = Set(storedBundles.compactMap { info -> String? in
|
|
693
|
-
let id = info.getId()
|
|
694
|
-
return id.isEmpty ? nil : id
|
|
695
|
-
})
|
|
696
|
-
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds, threadToCheck: Thread.current)
|
|
697
|
-
self.implementation.cleanupOrphanedTempFolders(threadToCheck: Thread.current)
|
|
698
|
-
|
|
699
|
-
// Check again before the expensive delta cache cleanup
|
|
700
|
-
if Thread.current.isCancelled {
|
|
701
|
-
self.logger.warn("Cleanup was cancelled before delta cache cleanup")
|
|
702
|
-
return
|
|
703
|
-
}
|
|
704
|
-
self.implementation.cleanupDeltaCache(threadToCheck: Thread.current)
|
|
1101
|
+
self.implementation.cleanupDeltaCache()
|
|
705
1102
|
}
|
|
706
|
-
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
707
|
-
UserDefaults.standard.synchronize()
|
|
708
|
-
}
|
|
709
|
-
cleanupThread?.start()
|
|
710
1103
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1104
|
+
// Resume any DELETING leftovers from prior kills, one-by-one.
|
|
1105
|
+
self.implementation.drainPendingDeletes()
|
|
1106
|
+
|
|
1107
|
+
// Always sweep orphan directories so incomplete prior cleanups (or failed deletes)
|
|
1108
|
+
// cannot leave hundreds of MB behind across launches.
|
|
1109
|
+
let allowedIds = self.implementation.allowedBundleIdsForCleanup()
|
|
1110
|
+
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds)
|
|
1111
|
+
self.implementation.cleanupOrphanedTempFolders(threadToCheck: nil)
|
|
1112
|
+
|
|
1113
|
+
if self.defaultChannelCleanupMustRetry {
|
|
1114
|
+
self.logger.warn("Keeping the previous native build version so default channel cleanup retries")
|
|
1115
|
+
} else {
|
|
1116
|
+
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
1117
|
+
UserDefaults.standard.synchronize()
|
|
718
1118
|
}
|
|
719
1119
|
}
|
|
1120
|
+
cleanupThread?.start()
|
|
720
1121
|
}
|
|
721
1122
|
|
|
722
1123
|
private func waitForCleanupIfNeeded() {
|
|
@@ -725,11 +1126,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
725
1126
|
}
|
|
726
1127
|
|
|
727
1128
|
logger.info("Waiting for cleanup to complete before starting download...")
|
|
728
|
-
|
|
729
|
-
// Wait for cleanup to complete - blocks until lock is released
|
|
730
|
-
cleanupLock.lock()
|
|
731
|
-
cleanupLock.unlock()
|
|
732
|
-
|
|
1129
|
+
cleanupGroup.wait()
|
|
733
1130
|
logger.info("Cleanup finished, proceeding with download")
|
|
734
1131
|
}
|
|
735
1132
|
|
|
@@ -1146,6 +1543,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1146
1543
|
}
|
|
1147
1544
|
|
|
1148
1545
|
private func downloadBundle(urlString: String, version: String, sessionKey: String, checksum rawChecksum: String, manifestEntries: [ManifestEntry]?) throws -> BundleInfo {
|
|
1546
|
+
// Manual/preview downloads must wait too - launch orphan sweep can delete their temps.
|
|
1547
|
+
self.waitForCleanupIfNeeded()
|
|
1149
1548
|
guard let url = URL(string: urlString) else {
|
|
1150
1549
|
throw makePreviewError("Invalid download URL")
|
|
1151
1550
|
}
|
|
@@ -1297,6 +1696,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1297
1696
|
|
|
1298
1697
|
let performReload: () -> Bool = {
|
|
1299
1698
|
guard self.applyCurrentBundleToBridge(bridge) else {
|
|
1699
|
+
self.clearPendingNotifyAppReady()
|
|
1300
1700
|
return false
|
|
1301
1701
|
}
|
|
1302
1702
|
self.checkAppReady()
|
|
@@ -1501,8 +1901,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1501
1901
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
1502
1902
|
}
|
|
1503
1903
|
|
|
1904
|
+
let previousDefaultChannel = self.persistedDefaultChannel()
|
|
1905
|
+
guard self.persistDefaultChannelPreviewSnapshot(channel: previousDefaultChannel) else {
|
|
1906
|
+
logger.error("Could not durably save the default channel preview snapshot")
|
|
1907
|
+
return false
|
|
1908
|
+
}
|
|
1504
1909
|
UserDefaults.standard.set(self.implementation.appId, forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1505
|
-
if let previousDefaultChannel =
|
|
1910
|
+
if let previousDefaultChannel = previousDefaultChannel {
|
|
1506
1911
|
UserDefaults.standard.set(previousDefaultChannel, forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1507
1912
|
UserDefaults.standard.set(true, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1508
1913
|
} else {
|
|
@@ -1950,7 +2355,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1950
2355
|
?? self.getBooleanConfig("allowShakeChannelSelector", defaultValue: false)
|
|
1951
2356
|
self.restorePreviewPreviousNextBundle()
|
|
1952
2357
|
self.restorePreviewPreviousAppId()
|
|
1953
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2358
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2359
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2360
|
+
}
|
|
1954
2361
|
|
|
1955
2362
|
self.previewSessionEnabled = false
|
|
1956
2363
|
self.previewSessionAlertPending = false
|
|
@@ -1977,7 +2384,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1977
2384
|
|
|
1978
2385
|
self.restorePreviewPreviousNextBundle()
|
|
1979
2386
|
self.restorePreviewPreviousAppId()
|
|
1980
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2387
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2388
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2389
|
+
}
|
|
1981
2390
|
self.previewSessionEnabled = false
|
|
1982
2391
|
self.previewSessionAlertPending = false
|
|
1983
2392
|
self.isLeavingPreviewForIncomingLink = false
|
|
@@ -2005,14 +2414,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2005
2414
|
}
|
|
2006
2415
|
|
|
2007
2416
|
private func clearPreviewSessionPreferences() {
|
|
2417
|
+
let keepDefaultChannelSnapshotFallback = self.hasPendingDefaultChannelPreviewSnapshot()
|
|
2008
2418
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2009
2419
|
UserDefaults.standard.removeObject(forKey: self.previewSessionDefaultsKey)
|
|
2010
2420
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeMenuDefaultsKey)
|
|
2011
2421
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
2012
2422
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
2013
2423
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousAppIdDefaultsKey)
|
|
2014
|
-
|
|
2015
|
-
|
|
2424
|
+
if !keepDefaultChannelSnapshotFallback {
|
|
2425
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
2426
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
2427
|
+
}
|
|
2016
2428
|
UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
|
|
2017
2429
|
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
2018
2430
|
UserDefaults.standard.removeObject(forKey: self.previewNameDefaultsKey)
|
|
@@ -2030,21 +2442,95 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2030
2442
|
logger.info("Restored appId after preview: \(previousAppId)")
|
|
2031
2443
|
}
|
|
2032
2444
|
|
|
2033
|
-
private func restorePreviewPreviousDefaultChannel() {
|
|
2034
|
-
|
|
2035
|
-
|
|
2445
|
+
private func restorePreviewPreviousDefaultChannel() -> Bool {
|
|
2446
|
+
guard !defaultChannelCleanupMustRetry else {
|
|
2447
|
+
logger.warn("Skipping default channel preview restore until cleanup retries")
|
|
2448
|
+
return false
|
|
2449
|
+
}
|
|
2450
|
+
guard let stateFile = self.defaultChannelStateFile(),
|
|
2451
|
+
let previewSnapshotFile = self.defaultChannelPreviewSnapshotFile() else {
|
|
2452
|
+
return false
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
let previousDefaultChannel: String?
|
|
2456
|
+
switch self.defaultChannelPreviewSnapshot(file: previewSnapshotFile) {
|
|
2457
|
+
case .invalidated:
|
|
2458
|
+
return true
|
|
2459
|
+
case let .snapshot(channel):
|
|
2460
|
+
previousDefaultChannel = channel
|
|
2461
|
+
case .missing:
|
|
2462
|
+
let hadPreviousDefaultChannel = UserDefaults.standard.object(
|
|
2463
|
+
forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey
|
|
2464
|
+
) as? Bool ?? false
|
|
2465
|
+
previousDefaultChannel = hadPreviousDefaultChannel
|
|
2466
|
+
? UserDefaults.standard.string(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
2467
|
+
: nil
|
|
2468
|
+
do {
|
|
2469
|
+
try self.persistDefaultChannelPreviewSnapshot(
|
|
2470
|
+
channel: previousDefaultChannel,
|
|
2471
|
+
file: previewSnapshotFile
|
|
2472
|
+
)
|
|
2473
|
+
} catch {
|
|
2474
|
+
logger.warn("Cannot migrate default channel preview snapshot: \(error.localizedDescription)")
|
|
2475
|
+
return false
|
|
2476
|
+
}
|
|
2477
|
+
case .unreadable:
|
|
2478
|
+
logger.warn("Cannot restore default channel from an unreadable preview snapshot")
|
|
2479
|
+
return false
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
return self.persistRestoredDefaultChannelFromPreview(
|
|
2483
|
+
channel: previousDefaultChannel,
|
|
2484
|
+
stateFile: stateFile,
|
|
2485
|
+
previewSnapshotFile: previewSnapshotFile
|
|
2486
|
+
)
|
|
2487
|
+
}
|
|
2036
2488
|
|
|
2037
|
-
|
|
2038
|
-
|
|
2489
|
+
private func applyRestoredDefaultChannelFromPreview(_ channel: String?) {
|
|
2490
|
+
let configDefaultChannel = self.getStringConfig("defaultChannel", defaultValue: "")
|
|
2491
|
+
if let channel = channel {
|
|
2492
|
+
UserDefaults.standard.set(channel, forKey: self.defaultChannelDefaultsKey)
|
|
2493
|
+
self.implementation.defaultChannel = channel
|
|
2494
|
+
logger.info("Restored defaultChannel after preview")
|
|
2495
|
+
} else {
|
|
2039
2496
|
UserDefaults.standard.removeObject(forKey: self.defaultChannelDefaultsKey)
|
|
2040
2497
|
self.implementation.defaultChannel = configDefaultChannel
|
|
2041
2498
|
logger.info("Restored defaultChannel after preview to config value")
|
|
2042
|
-
return
|
|
2043
2499
|
}
|
|
2500
|
+
}
|
|
2044
2501
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2502
|
+
private func persistRestoredDefaultChannelFromPreview(
|
|
2503
|
+
channel: String?,
|
|
2504
|
+
stateFile: URL,
|
|
2505
|
+
previewSnapshotFile: URL
|
|
2506
|
+
) -> Bool {
|
|
2507
|
+
defaultChannelStateLock.lock()
|
|
2508
|
+
defer { defaultChannelStateLock.unlock() }
|
|
2509
|
+
let didPersist: Bool
|
|
2510
|
+
do {
|
|
2511
|
+
try self.persistDefaultChannelStateWithoutLock(channel: channel, file: stateFile)
|
|
2512
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
2513
|
+
channel: nil,
|
|
2514
|
+
isValid: false,
|
|
2515
|
+
file: previewSnapshotFile
|
|
2516
|
+
)
|
|
2517
|
+
didPersist = true
|
|
2518
|
+
} catch {
|
|
2519
|
+
let persistedState = self.defaultChannelStateWithoutLock(file: stateFile)
|
|
2520
|
+
let persistedSnapshot = self.defaultChannelPreviewSnapshotWithoutLock(file: previewSnapshotFile)
|
|
2521
|
+
if persistedState.exists,
|
|
2522
|
+
persistedState.isReadable,
|
|
2523
|
+
persistedState.channel == channel,
|
|
2524
|
+
persistedSnapshot == .invalidated {
|
|
2525
|
+
logger.warn("Default channel preview restore committed before metadata update failed")
|
|
2526
|
+
didPersist = true
|
|
2527
|
+
} else {
|
|
2528
|
+
logger.warn("Cannot durably restore default channel after preview: \(error.localizedDescription)")
|
|
2529
|
+
didPersist = false
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
self.applyRestoredDefaultChannelFromPreview(channel)
|
|
2533
|
+
return didPersist
|
|
2048
2534
|
}
|
|
2049
2535
|
|
|
2050
2536
|
private func normalizedPreviewAppId(_ rawAppId: String?) -> String? {
|
|
@@ -2224,7 +2710,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2224
2710
|
self.shakeMenuGesture = Self.normalizedShakeMenuGesture(self.getStringConfig("shakeMenuGesture", defaultValue: Self.shakeMenuGestureShake))
|
|
2225
2711
|
self.syncShakeMenuGestureRecognizer()
|
|
2226
2712
|
self.restorePreviewPreviousAppId()
|
|
2227
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2713
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2714
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2715
|
+
}
|
|
2228
2716
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2229
2717
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
2230
2718
|
self.clearPreviewSessionPreferences()
|
|
@@ -2469,6 +2957,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2469
2957
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
2470
2958
|
])
|
|
2471
2959
|
} else {
|
|
2960
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
2961
|
+
self.rejectCall(call, message: "Channel override removed but local persistence failed", code: "UNSETCHANNEL_PERSISTENCE_FAILED")
|
|
2962
|
+
return
|
|
2963
|
+
}
|
|
2472
2964
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
2473
2965
|
self.logger.info("Calling autoupdater after channel change!")
|
|
2474
2966
|
// Check if download is already in progress (with timeout protection)
|
|
@@ -2515,6 +3007,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2515
3007
|
"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"
|
|
2516
3008
|
])
|
|
2517
3009
|
} else {
|
|
3010
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
3011
|
+
self.rejectCall(call, message: "Channel changed but local persistence failed", code: "SETCHANNEL_PERSISTENCE_FAILED")
|
|
3012
|
+
return
|
|
3013
|
+
}
|
|
2518
3014
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
2519
3015
|
self.logger.info("Calling autoupdater after channel change!")
|
|
2520
3016
|
// Check if download is already in progress (with timeout protection)
|
|
@@ -2539,6 +3035,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2539
3035
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
2540
3036
|
])
|
|
2541
3037
|
} else {
|
|
3038
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
3039
|
+
self.rejectCall(call, message: "Channel synchronized but local persistence failed", code: "GETCHANNEL_PERSISTENCE_FAILED")
|
|
3040
|
+
return
|
|
3041
|
+
}
|
|
2542
3042
|
self.resolveCall(call, data: res.toDict())
|
|
2543
3043
|
}
|
|
2544
3044
|
}
|
|
@@ -2851,12 +3351,21 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2851
3351
|
self.implementation.setError(bundle: current)
|
|
2852
3352
|
_ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
|
|
2853
3353
|
if self.autoDeleteFailed && !current.isBuiltin() {
|
|
3354
|
+
let failedId = current.getId()
|
|
2854
3355
|
logger.info("Deleting failing bundle: \(current.toString())")
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3356
|
+
// Mark before async work so kill/OOM still resumes via drainPendingDeletes.
|
|
3357
|
+
self.implementation.saveBundleInfo(
|
|
3358
|
+
id: failedId,
|
|
3359
|
+
bundle: current.setStatus(status: BundleStatus.DELETING.storedValue)
|
|
3360
|
+
)
|
|
3361
|
+
UserDefaults.standard.synchronize()
|
|
3362
|
+
DispatchQueue.global(qos: .utility).async {
|
|
3363
|
+
let res = self.implementation.delete(id: failedId, removeInfo: false)
|
|
3364
|
+
if res {
|
|
3365
|
+
self.logger.info("Failed bundle deleted: \(failedId)")
|
|
3366
|
+
} else {
|
|
3367
|
+
self.logger.error("Failed to delete failed bundle: \(failedId)")
|
|
3368
|
+
}
|
|
2860
3369
|
}
|
|
2861
3370
|
}
|
|
2862
3371
|
} else {
|
|
@@ -2881,7 +3390,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2881
3390
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
2882
3391
|
logger.info("sendReadyToJs")
|
|
2883
3392
|
DispatchQueue.global().async {
|
|
2884
|
-
|
|
3393
|
+
// Only wait after load()/reload() armed the semaphore. Foreground resumes
|
|
3394
|
+
// with autoUpdate disabled call sendReadyToJs again without a fresh
|
|
3395
|
+
// notifyAppReady(), so waiting there always timed out after appReadyTimeout.
|
|
3396
|
+
if self.consumePendingNotifyAppReady() {
|
|
3397
|
+
self.semaphoreWait(waitTime: self.appReadyTimeout)
|
|
3398
|
+
}
|
|
2885
3399
|
self.notifyListeners("appReady", data: ["bundle": current.toJSON(), "status": msg], retainUntilConsumed: true)
|
|
2886
3400
|
|
|
2887
3401
|
// Auto hide splashscreen if enabled
|
|
@@ -2893,6 +3407,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2893
3407
|
}
|
|
2894
3408
|
}
|
|
2895
3409
|
|
|
3410
|
+
private func consumePendingNotifyAppReady() -> Bool {
|
|
3411
|
+
let shouldWait = pendingNotifyAppReady
|
|
3412
|
+
pendingNotifyAppReady = false
|
|
3413
|
+
return shouldWait
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
private func clearPendingNotifyAppReady() {
|
|
3417
|
+
pendingNotifyAppReady = false
|
|
3418
|
+
}
|
|
3419
|
+
|
|
2896
3420
|
private func hideSplashscreen() {
|
|
2897
3421
|
if Thread.isMainThread {
|
|
2898
3422
|
self.performHideSplashscreen()
|
|
@@ -3506,6 +4030,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3506
4030
|
self.currentBuildVersion = currentBuildVersion
|
|
3507
4031
|
}
|
|
3508
4032
|
|
|
4033
|
+
func setAppReadyTimeoutForTesting(_ timeout: Int) {
|
|
4034
|
+
self.appReadyTimeout = timeout
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
func armPendingNotifyAppReadyForTesting() {
|
|
4038
|
+
pendingNotifyAppReady = true
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
func clearPendingNotifyAppReadyForTesting() {
|
|
4042
|
+
clearPendingNotifyAppReady()
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
var isPendingNotifyAppReadyForTesting: Bool {
|
|
4046
|
+
pendingNotifyAppReady
|
|
4047
|
+
}
|
|
4048
|
+
|
|
3509
4049
|
func shouldUseDirectUpdateForTesting() -> Bool {
|
|
3510
4050
|
self.shouldUseDirectUpdate()
|
|
3511
4051
|
}
|