Review ProfileManager observation logic (#809)
- Perform profiles removal in a single publisher, in reloadRemoteProfiles() after importing remote profiles - Only force a new lastUpdate/fingerprint if profile is saved locally, DO NOT alter them if imported from remote repository because this would cause a re-save on iCloud - Profiles were purged twice on launch in the main macOS app
This commit is contained in:
parent
d37194a9f9
commit
0c66050726
|
@ -30,7 +30,7 @@ import SwiftUI
|
||||||
|
|
||||||
extension AppDelegate: UIApplicationDelegate {
|
extension AppDelegate: UIApplicationDelegate {
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||||
configure(with: AppUIMain())
|
configure(with: AppUIMain(isStartedFromLoginItem: false))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ import SwiftUI
|
||||||
|
|
||||||
extension AppDelegate: NSApplicationDelegate {
|
extension AppDelegate: NSApplicationDelegate {
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
configure(with: AppUIMain())
|
configure(with: AppUIMain(isStartedFromLoginItem: isStartedFromLoginItem))
|
||||||
hideIfLoginItem()
|
hideIfLoginItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,11 +27,21 @@ import Foundation
|
||||||
@_exported import UILibrary
|
@_exported import UILibrary
|
||||||
|
|
||||||
public final class AppUIMain: UILibraryConfiguring {
|
public final class AppUIMain: UILibraryConfiguring {
|
||||||
public init() {
|
private let isStartedFromLoginItem: Bool
|
||||||
|
|
||||||
|
public init(isStartedFromLoginItem: Bool) {
|
||||||
|
self.isStartedFromLoginItem = isStartedFromLoginItem
|
||||||
}
|
}
|
||||||
|
|
||||||
public func configure(with context: AppContext) {
|
public func configure(with context: AppContext) {
|
||||||
assertMissingImplementations()
|
assertMissingImplementations()
|
||||||
|
|
||||||
|
// keep this for login item because scenePhase is not triggered
|
||||||
|
if isStartedFromLoginItem {
|
||||||
|
Task {
|
||||||
|
try await context.tunnel.prepare(purge: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,9 @@ private extension NEProfileRepository {
|
||||||
"\($0.name)(\($0.id)"
|
"\($0.name)(\($0.id)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !removedProfilesDescription.isEmpty {
|
||||||
pp_log(.app, .info, "Sync profiles removed externally: \(removedProfilesDescription)")
|
pp_log(.app, .info, "Sync profiles removed externally: \(removedProfilesDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
profilesSubject.send(profiles)
|
profilesSubject.send(profiles)
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,42 +130,51 @@ extension ProfileManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func save(_ profile: Profile, isShared: Bool? = nil) async throws {
|
public func save(_ originalProfile: Profile, force: Bool = false, isShared: Bool? = nil) async throws {
|
||||||
|
let profile: Profile
|
||||||
// inject attributes
|
if force {
|
||||||
var builder = profile.builder()
|
var builder = originalProfile.builder()
|
||||||
builder.attributes.lastUpdate = Date()
|
builder.attributes.lastUpdate = Date()
|
||||||
builder.attributes.fingerprint = UUID()
|
builder.attributes.fingerprint = UUID()
|
||||||
let historifiedProfile = try builder.tryBuild()
|
profile = try builder.tryBuild()
|
||||||
|
} else {
|
||||||
|
profile = originalProfile
|
||||||
|
}
|
||||||
|
|
||||||
pp_log(.app, .notice, "Save profile \(historifiedProfile.id)...")
|
pp_log(.app, .notice, "Save profile \(profile.id)...")
|
||||||
do {
|
do {
|
||||||
try await repository.saveProfile(historifiedProfile)
|
let existingProfile = allProfiles[profile.id]
|
||||||
|
if existingProfile == nil || profile != existingProfile {
|
||||||
|
try await repository.saveProfile(profile)
|
||||||
if let backupRepository {
|
if let backupRepository {
|
||||||
Task.detached {
|
Task.detached {
|
||||||
try await backupRepository.saveProfile(historifiedProfile)
|
try await backupRepository.saveProfile(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allProfiles[historifiedProfile.id] = historifiedProfile
|
allProfiles[profile.id] = profile
|
||||||
didChange.send(.save(historifiedProfile))
|
didChange.send(.save(profile))
|
||||||
|
} else {
|
||||||
|
pp_log(.app, .notice, "\tProfile \(profile.id) not modified, not saving")
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .fault, "Unable to save profile \(historifiedProfile.id): \(error)")
|
pp_log(.app, .fault, "\tUnable to save profile \(profile.id): \(error)")
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
if let isShared, let remoteRepository {
|
if let isShared, let remoteRepository {
|
||||||
if isShared {
|
if isShared {
|
||||||
pp_log(.app, .notice, "Enable remote sharing of profile \(historifiedProfile.id)...")
|
pp_log(.app, .notice, "\tEnable remote sharing of profile \(profile.id)...")
|
||||||
try await remoteRepository.saveProfile(historifiedProfile)
|
try await remoteRepository.saveProfile(profile)
|
||||||
} else {
|
} else {
|
||||||
pp_log(.app, .notice, "Disable remote sharing of profile \(historifiedProfile.id)...")
|
pp_log(.app, .notice, "\tDisable remote sharing of profile \(profile.id)...")
|
||||||
try await remoteRepository.removeProfiles(withIds: [historifiedProfile.id])
|
try await remoteRepository.removeProfiles(withIds: [profile.id])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .fault, "Unable to save/remove remote profile \(historifiedProfile.id): \(error)")
|
pp_log(.app, .fault, "\tUnable to save/remove remote profile \(profile.id): \(error)")
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
pp_log(.app, .notice, "Finished saving profile \(profile.id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func remove(withId profileId: Profile.ID) async {
|
public func remove(withId profileId: Profile.ID) async {
|
||||||
|
@ -272,19 +281,24 @@ extension ProfileManager {
|
||||||
}
|
}
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
remoteRepository?
|
// observe remote after first local profiles
|
||||||
|
let remotePublisher = remoteRepository?
|
||||||
.profilesPublisher
|
.profilesPublisher
|
||||||
|
.zip(repository.profilesPublisher)
|
||||||
|
|
||||||
|
remotePublisher?
|
||||||
|
.first()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] in
|
.sink { [weak self] remote, _ in
|
||||||
self?.reloadRemoteProfiles($0)
|
self?.loadInitialRemoteProfiles(remote)
|
||||||
}
|
}
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
remoteRepository?
|
remotePublisher?
|
||||||
.profilesPublisher
|
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.sink { [weak self] in
|
.receive(on: DispatchQueue.main)
|
||||||
self?.importRemoteProfiles($0)
|
.sink { [weak self] remote, _ in
|
||||||
|
self?.reloadRemoteProfiles(remote)
|
||||||
}
|
}
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
|
@ -304,6 +318,7 @@ private extension ProfileManager {
|
||||||
$0[$1.id] = $1
|
$0[$1.id] = $1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// should not be imported at all, but you never know
|
||||||
if let isIncluded {
|
if let isIncluded {
|
||||||
let idsToRemove: [Profile.ID] = allProfiles
|
let idsToRemove: [Profile.ID] = allProfiles
|
||||||
.filter {
|
.filter {
|
||||||
|
@ -312,7 +327,7 @@ private extension ProfileManager {
|
||||||
.map(\.key)
|
.map(\.key)
|
||||||
|
|
||||||
if !idsToRemove.isEmpty {
|
if !idsToRemove.isEmpty {
|
||||||
pp_log(.app, .info, "Delete non-included local profile: \(idsToRemove)")
|
pp_log(.app, .info, "Delete non-included local profiles: \(idsToRemove)")
|
||||||
Task.detached {
|
Task.detached {
|
||||||
try await self.repository.removeProfiles(withIds: idsToRemove)
|
try await self.repository.removeProfiles(withIds: idsToRemove)
|
||||||
}
|
}
|
||||||
|
@ -320,54 +335,62 @@ private extension ProfileManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadInitialRemoteProfiles(_ result: [Profile]) {
|
||||||
|
pp_log(.app, .info, "Load initial remote profiles: \(result.map(\.id))")
|
||||||
|
allRemoteProfiles = result.reduce(into: [:]) {
|
||||||
|
$0[$1.id] = $1
|
||||||
|
}
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
|
|
||||||
func reloadRemoteProfiles(_ result: [Profile]) {
|
func reloadRemoteProfiles(_ result: [Profile]) {
|
||||||
pp_log(.app, .info, "Reload remote profiles: \(result.map(\.id))")
|
pp_log(.app, .info, "Reload remote profiles: \(result.map(\.id))")
|
||||||
allRemoteProfiles = result.reduce(into: [:]) {
|
allRemoteProfiles = result.reduce(into: [:]) {
|
||||||
$0[$1.id] = $1
|
$0[$1.id] = $1
|
||||||
}
|
}
|
||||||
|
|
||||||
if deletingRemotely {
|
|
||||||
let idsToRemove = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
|
|
||||||
if !idsToRemove.isEmpty {
|
|
||||||
pp_log(.app, .info, "Delete local profiles removed remotely: \(idsToRemove)")
|
|
||||||
Task.detached {
|
|
||||||
try await self.repository.removeProfiles(withIds: Array(idsToRemove))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
|
||||||
|
|
||||||
// pull remote updates into local profiles (best-effort)
|
|
||||||
func importRemoteProfiles(_ result: [Profile]) {
|
|
||||||
let profilesToImport = result
|
let profilesToImport = result
|
||||||
pp_log(.app, .info, "Try to import remote profiles: \(result.map(\.id))")
|
|
||||||
|
|
||||||
let allFingerprints = allProfiles.values.reduce(into: [:]) {
|
let allFingerprints = allProfiles.values.reduce(into: [:]) {
|
||||||
$0[$1.id] = $1.attributes.fingerprint
|
$0[$1.id] = $1.attributes.fingerprint
|
||||||
}
|
}
|
||||||
|
let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
|
||||||
|
let deletingRemotely = deletingRemotely
|
||||||
|
|
||||||
Task.detached { [weak self] in
|
Task.detached { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pp_log(.app, .info, "Start importing remote profiles...")
|
||||||
|
var idsToRemove: [Profile.ID] = []
|
||||||
|
if !remotelyDeletedIds.isEmpty {
|
||||||
|
pp_log(.app, .info, "\tWill \(deletingRemotely ? "delete" : "retain") local profiles not present in remote repository: \(remotelyDeletedIds)")
|
||||||
|
|
||||||
|
if deletingRemotely {
|
||||||
|
idsToRemove.append(contentsOf: remotelyDeletedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
for remoteProfile in profilesToImport {
|
for remoteProfile in profilesToImport {
|
||||||
do {
|
do {
|
||||||
guard self?.isIncluded?(remoteProfile) ?? true else {
|
guard isIncluded?(remoteProfile) ?? true else {
|
||||||
pp_log(.app, .info, "Delete non-included remote profile \(remoteProfile.id)")
|
pp_log(.app, .info, "\tWill delete non-included remote profile \(remoteProfile.id)")
|
||||||
try? await self?.repository.removeProfiles(withIds: [remoteProfile.id])
|
idsToRemove.append(remoteProfile.id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if let localFingerprint = allFingerprints[remoteProfile.id] {
|
if let localFingerprint = allFingerprints[remoteProfile.id] {
|
||||||
guard remoteProfile.attributes.fingerprint != localFingerprint else {
|
guard remoteProfile.attributes.fingerprint != localFingerprint else {
|
||||||
pp_log(.app, .info, "Skip re-importing local profile \(remoteProfile.id)")
|
pp_log(.app, .info, "\tSkip re-importing local profile \(remoteProfile.id)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pp_log(.app, .notice, "Import remote profile \(remoteProfile.id)...")
|
pp_log(.app, .notice, "\tImport remote profile \(remoteProfile.id)...")
|
||||||
try await self?.save(remoteProfile)
|
try await save(remoteProfile)
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .error, "Unable to import remote profile: \(error)")
|
pp_log(.app, .error, "\tUnable to import remote profile: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pp_log(.app, .notice, "Finished importing remote profiles, delete stale profiles: \(idsToRemove)")
|
||||||
|
try? await repository.removeProfiles(withIds: idsToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,10 @@ public struct ProfileAttributes: Hashable, Codable {
|
||||||
self.lastUpdate = lastUpdate
|
self.lastUpdate = lastUpdate
|
||||||
self.fingerprint = fingerprint
|
self.fingerprint = fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isEquivalent(to other: Self) -> Bool {
|
||||||
|
isAvailableForTV == other.isAvailableForTV
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: #570, test user info encoding/decoding with JSONSerialization
|
// FIXME: #570, test user info encoding/decoding with JSONSerialization
|
||||||
|
|
|
@ -198,7 +198,7 @@ extension ProfileEditor {
|
||||||
public func save(to profileManager: ProfileManager) async throws {
|
public func save(to profileManager: ProfileManager) async throws {
|
||||||
do {
|
do {
|
||||||
let newProfile = try build()
|
let newProfile = try build()
|
||||||
try await profileManager.save(newProfile, isShared: isShared)
|
try await profileManager.save(newProfile, force: true, isShared: isShared)
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.app, .fault, "Unable to save edited profile: \(error)")
|
pp_log(.app, .fault, "Unable to save edited profile: \(error)")
|
||||||
throw error
|
throw error
|
||||||
|
|
|
@ -45,15 +45,9 @@ public final class UILibrary: UILibraryConfiguring {
|
||||||
parameters: Constants.shared.log,
|
parameters: Constants.shared.log,
|
||||||
logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key)
|
logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key)
|
||||||
)
|
)
|
||||||
|
|
||||||
uiConfiguring?.configure(with: context)
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
try await context.providerManager.fetchIndex(from: API.shared)
|
try await context.providerManager.fetchIndex(from: API.shared)
|
||||||
#if os(macOS)
|
|
||||||
// keep this for login item because scenePhase is not triggered
|
|
||||||
try await context.tunnel.prepare(purge: true)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
uiConfiguring?.configure(with: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue