Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make remote config accessible to background agents #3124

Merged
merged 44 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
799b044
Convert app to use default configuration manager
SlayterDev Aug 14, 2024
2d36d74
Setup configurationmanager in DBP agent
SlayterDev Aug 15, 2024
d7e1d5c
Migrate DBP to real privacy config
SlayterDev Aug 19, 2024
1de0973
Move config log to BSK
SlayterDev Aug 19, 2024
129bdf7
Draft code for pixel test
SlayterDev Aug 20, 2024
b0efc67
Setup file dispatch to observe file changes
SlayterDev Aug 21, 2024
95f9515
Add VPN config setup
SlayterDev Aug 21, 2024
35dd9fd
Merge branch 'main' into brad/background-config
SlayterDev Aug 22, 2024
739bd8d
Migrate to new logging system
SlayterDev Aug 22, 2024
3abce52
Migrate to config app group
SlayterDev Aug 22, 2024
4bd5573
Refactor to use NSFileCoordinator and KeyValueStoring
SlayterDev Aug 27, 2024
2a105c5
Refactoring
SlayterDev Aug 28, 2024
13a4ca3
Merge branch 'main' into brad/background-config
SlayterDev Aug 28, 2024
73995f5
Refactor away from singletons
SlayterDev Aug 29, 2024
0106b03
Merge branch 'main' into brad/background-config
SlayterDev Aug 30, 2024
636d565
Fix tests
SlayterDev Aug 30, 2024
998fb7a
More test fixing
SlayterDev Sep 3, 2024
aa752c3
Merge branch 'main' into brad/background-config
SlayterDev Sep 3, 2024
e928aea
Remove revived logging file
SlayterDev Sep 3, 2024
bb636af
Update config managers to watch directory
SlayterDev Sep 4, 2024
1e32b7e
Use BSK commit
SlayterDev Sep 4, 2024
65fc13e
Add libraries to targets
SlayterDev Sep 4, 2024
a8994ff
Merge branch 'main' into brad/background-config
SlayterDev Sep 4, 2024
378d80d
Trying to fix test compilation failure
SlayterDev Sep 4, 2024
8774ed5
Add more libraries
SlayterDev Sep 4, 2024
2c7e3c3
Link BSK
SlayterDev Sep 4, 2024
f827478
Link and lint
SlayterDev Sep 4, 2024
44f440a
Fix mock
SlayterDev Sep 5, 2024
4077d9e
More library linking
SlayterDev Sep 5, 2024
83d5eff
Fix TODOs
SlayterDev Sep 5, 2024
1d30e02
Compilation error
SlayterDev Sep 5, 2024
897f9ab
Make pixel test one time only
SlayterDev Sep 5, 2024
06ee6ae
Switch to real config urls
SlayterDev Sep 5, 2024
b214d59
Merge branch 'main' into brad/background-config
SlayterDev Sep 9, 2024
5ef8354
Merge branch 'main' into brad/background-config
SlayterDev Sep 11, 2024
31e230f
Merge branch 'main' into brad/background-config
SlayterDev Sep 13, 2024
036d3d3
PR comments
SlayterDev Sep 13, 2024
c2d91e8
lint
SlayterDev Sep 13, 2024
0265f30
Fix tests
SlayterDev Sep 13, 2024
054335f
Fix for release build
SlayterDev Sep 13, 2024
11b12c8
Merge branch 'main' into brad/background-config
SlayterDev Sep 16, 2024
8103be0
Update BSK
SlayterDev Sep 16, 2024
9a87603
Merge branch 'main' into brad/background-config
SlayterDev Sep 17, 2024
163a815
PR nits
SlayterDev Sep 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 156 additions & 2 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
"branch" : "194.0.0",
"revision" : "026acbd36fb80c95e0bfc6a9080e369dd85db66f"
"revision" : "657a961c94c9c45f04ecd7c0937caf84cbedc731"
}
},
{
Expand Down
13 changes: 9 additions & 4 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

public let vpnSettings = VPNSettings(defaults: .netP)

var configurationStore = ConfigurationStore()
var configurationManager: ConfigurationManager

// MARK: - VPN

private var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler?
Expand Down Expand Up @@ -174,6 +177,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore)
internalUserDecider = DefaultInternalUserDecider(store: internalUserDeciderStore)

configurationManager = ConfigurationManager(store: configurationStore)

if NSApplication.runType.requiresEnvironment {
Self.configurePixelKit()

Expand Down Expand Up @@ -221,10 +226,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
#if DEBUG
AppPrivacyFeatures.shared = NSApplication.runType.requiresEnvironment
// runtime mock-replacement for Unit Tests, to be redone when we‘ll be doing Dependency Injection
? AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider), database: Database.shared)
? AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider, configurationStore: configurationStore), database: Database.shared)
: AppPrivacyFeatures(contentBlocking: ContentBlockingMock(), httpsUpgradeStore: HTTPSUpgradeStoreMock())
#else
AppPrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider), database: Database.shared)
AppPrivacyFeatures.shared = AppPrivacyFeatures(contentBlocking: AppContentBlocking(internalUserDecider: internalUserDecider, configurationStore: configurationStore), database: Database.shared)
#endif
if NSApplication.runType.requiresEnvironment {
remoteMessagingClient = RemoteMessagingClient(
Expand All @@ -233,7 +238,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
appearancePreferences: .shared,
pinnedTabsManager: pinnedTabsManager,
internalUserDecider: internalUserDecider,
configurationStore: ConfigurationStore.shared,
configurationStore: configurationStore,
remoteMessagingAvailabilityProvider: PrivacyConfigurationRemoteMessagingAvailabilityProvider(
privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager
)
Expand Down Expand Up @@ -310,7 +315,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
if case .normal = NSApp.runType {
FaviconManager.shared.loadFavicons()
}
ConfigurationManager.shared.start()
configurationManager.start()
_ = DownloadListCoordinator.shared
_ = RecentlyClosedCoordinator.shared

Expand Down
11 changes: 0 additions & 11 deletions DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,6 @@ public struct UserDefaultsWrapper<T> {
/// system setting defining window title double-click action
case appleActionOnDoubleClick = "AppleActionOnDoubleClick"

case configLastUpdated = "config.last.updated"
case configStorageTrackerRadarEtag = "config.storage.trackerradar.etag"
case configStorageBloomFilterSpecEtag = "config.storage.bloomfilter.spec.etag"
case configStorageBloomFilterBinaryEtag = "config.storage.bloomfilter.binary.etag"
case configStorageBloomFilterExclusionsEtag = "config.storage.bloomfilter.exclusions.etag"
case configStorageSurrogatesEtag = "config.storage.surrogates.etag"
case configStoragePrivacyConfigurationEtag = "config.storage.privacyconfiguration.etag"
case configStorageRemoteMessagingConfigEtag = "config.storage.remotemessagingconfig.etag"

case configLastInstalled = "config.last.installed"

case fireproofDomains = "com.duckduckgo.fireproofing.allowedDomains"
case areDomainsMigratedToETLDPlus1 = "com.duckduckgo.are-domains-migrated-to-etldplus1"
case unprotectedDomains = "com.duckduckgo.contentblocker.unprotectedDomains"
Expand Down
148 changes: 54 additions & 94 deletions DuckDuckGo/Configuration/ConfigurationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,60 +17,32 @@
//

import Foundation
import os.log
import Combine
import BrowserServicesKit
import Persistence
import Configuration
import Common
import Networking
import PixelKit
import os.log

final class ConfigurationManager {

enum Error: Swift.Error {

case timeout
case bloomFilterSpecNotFound
case bloomFilterBinaryNotFound
case bloomFilterPersistenceFailed
case bloomFilterExclusionsNotFound
case bloomFilterExclusionsPersistenceFailed

func withUnderlyingError(_ underlyingError: Swift.Error) -> Swift.Error {
let nsError = self as NSError
return NSError(domain: nsError.domain, code: nsError.code, userInfo: [NSUnderlyingErrorKey: underlyingError])
}
final class ConfigurationManager: DefaultConfigurationManager {

private enum Constants {
static let lastConfigurationInstallDateKey = "config.last.installed"
}

enum Constants {

static let downloadTimeoutSeconds = 60.0 * 5
#if DEBUG
static let refreshPeriodSeconds = 60.0 * 2 // 2 minutes
#else
static let refreshPeriodSeconds = 60.0 * 30 // 30 minutes
#endif
static let retryDelaySeconds = 60.0 * 60 * 1 // 1 hour delay before checking again if something went wrong last time
static let refreshCheckIntervalSeconds = 60.0 // check if we need a refresh every minute
private var defaults: KeyValueStoring

private(set) var lastConfigurationInstallDate: Date? {
get {
defaults.object(forKey: Constants.lastConfigurationInstallDateKey) as? Date
}
set {
defaults.set(newValue, forKey: Constants.lastConfigurationInstallDateKey)
}
}

static let shared = ConfigurationManager()
static let queue: DispatchQueue = DispatchQueue(label: "Configuration Manager")

@UserDefaultsWrapper(key: .configLastUpdated, defaultValue: .distantPast)
private(set) var lastUpdateTime: Date

@UserDefaultsWrapper(key: .configLastInstalled, defaultValue: nil)
private(set) var lastConfigurationInstallDate: Date?

private var timerCancellable: AnyCancellable?
private var lastRefreshCheckTime: Date = Date()

private lazy var fetcher = ConfigurationFetcher(store: ConfigurationStore.shared,
eventMapping: Self.configurationDebugEvents)

static let configurationDebugEvents = EventMapping<ConfigurationDebugEvents> { event, error, _, _ in
let domainEvent: GeneralPixel
switch event {
Expand All @@ -81,21 +53,19 @@ final class ConfigurationManager {
PixelKit.fire(DebugEvent(domainEvent, error: error))
}

func start() {
Logger.config.debug("Starting configuration refresh timer")
timerCancellable = Timer.publish(every: Constants.refreshCheckIntervalSeconds, on: .main, in: .default)
.autoconnect()
.receive(on: Self.queue)
.sink(receiveValue: { _ in
self.lastRefreshCheckTime = Date()
self.refreshIfNeeded()
})
Task {
await refreshNow()
}
override init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents),
store: ConfigurationStoring = ConfigurationStore(),
defaults: KeyValueStoring = UserDefaults.appConfiguration) {
self.defaults = defaults
super.init(fetcher: fetcher, store: store, defaults: defaults)
}

func log() {
Logger.config.log("last update \(String(describing: self.lastUpdateTime), privacy: .public)")
Logger.config.log("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)")
}

private func refreshNow(isDebug: Bool = false) async {
override public func refreshNow(isDebug: Bool = false) async {
let updateTrackerBlockingDependenciesTask = Task {
let didFetchAnyTrackerBlockingDependencies = await fetchTrackerBlockingDependencies(isDebug: isDebug)
if didFetchAnyTrackerBlockingDependencies {
Expand All @@ -116,7 +86,7 @@ final class ConfigurationManager {

let updateBloomFilterExclusionsTask = Task {
do {
try await fetcher.fetch(.bloomFilterExcludedDomains)
try await fetcher.fetch(.bloomFilterExcludedDomains, isDebug: isDebug)
try await updateBloomFilterExclusions()
tryAgainLater()
} catch {
Expand All @@ -128,7 +98,7 @@ final class ConfigurationManager {
await updateBloomFilterTask.value
await updateBloomFilterExclusionsTask.value

ConfigurationStore.shared.log()
(store as? ConfigurationStore)?.log()

Logger.config.info("last update \(String(describing: self.lastUpdateTime), privacy: .public)")
Logger.config.info("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)")
Expand All @@ -138,16 +108,18 @@ final class ConfigurationManager {
var didFetchAnyTrackerBlockingDependencies = false

var tasks = [Configuration: Task<(), Swift.Error>]()
tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet) }
tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates) }
tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet, isDebug: isDebug) }
tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates, isDebug: isDebug) }
tasks[.privacyConfiguration] = Task { try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) }

for (configuration, task) in tasks {
do {
try await task.value
didFetchAnyTrackerBlockingDependencies = true
} catch {
Logger.config.error("Failed to complete configuration update to \(configuration.rawValue): \(error.localizedDescription, privacy: .public)")
Logger.config.error(
"Failed to complete configuration update to \(configuration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)"
)
tryAgainSoon()
}
}
Expand All @@ -167,48 +139,20 @@ final class ConfigurationManager {
tryAgainSoon()
}

@discardableResult
public func refreshIfNeeded() -> Task<Void, Never>? {
guard isReadyToRefresh else {
Logger.config.debug("Configuration refresh is not needed at this time")
return nil
}
return Task {
await refreshNow()
}
}

private var isReadyToRefresh: Bool { Date().timeIntervalSince(lastUpdateTime) > Constants.refreshPeriodSeconds }

public func forceRefresh(isDebug: Bool = false) {
Task {
await refreshNow(isDebug: isDebug)
}
}

private func tryAgainLater() {
lastUpdateTime = Date()
}

private func tryAgainSoon() {
// Set the last update time to in the past so it triggers again sooner
lastUpdateTime = Date(timeIntervalSinceNow: Constants.refreshPeriodSeconds - Constants.retryDelaySeconds)
}

private func updateTrackerBlockingDependencies() {
lastConfigurationInstallDate = Date()
ContentBlocking.shared.trackerDataManager.reload(etag: ConfigurationStore.shared.loadEtag(for: .trackerDataSet),
data: ConfigurationStore.shared.loadData(for: .trackerDataSet))
ContentBlocking.shared.privacyConfigurationManager.reload(etag: ConfigurationStore.shared.loadEtag(for: .privacyConfiguration),
data: ConfigurationStore.shared.loadData(for: .privacyConfiguration))
ContentBlocking.shared.trackerDataManager.reload(etag: store.loadEtag(for: .trackerDataSet),
data: store.loadData(for: .trackerDataSet))
ContentBlocking.shared.privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration),
data: store.loadData(for: .privacyConfiguration))
ContentBlocking.shared.contentBlockingManager.scheduleCompilation()
}

private func updateBloomFilter() async throws {
guard let specData = ConfigurationStore.shared.loadData(for: .bloomFilterSpec) else {
guard let specData = store.loadData(for: .bloomFilterSpec) else {
throw Error.bloomFilterSpecNotFound
}
guard let bloomFilterData = ConfigurationStore.shared.loadData(for: .bloomFilterBinary) else {
guard let bloomFilterData = store.loadData(for: .bloomFilterBinary) else {
throw Error.bloomFilterBinaryNotFound
}
try await Task.detached {
Expand All @@ -224,7 +168,7 @@ final class ConfigurationManager {
}

private func updateBloomFilterExclusions() async throws {
guard let bloomFilterExclusions = ConfigurationStore.shared.loadData(for: .bloomFilterExcludedDomains) else {
guard let bloomFilterExclusions = store.loadData(for: .bloomFilterExcludedDomains) else {
throw Error.bloomFilterExclusionsNotFound
}
try await Task.detached {
Expand All @@ -239,3 +183,19 @@ final class ConfigurationManager {
}

}

extension ConfigurationManager {
override var presentedItemURL: URL? {
store.fileUrl(for: .privacyConfiguration).deletingLastPathComponent()
}

override func presentedSubitemDidAppear(at url: URL) {
guard url == store.fileUrl(for: .privacyConfiguration) else { return }
updateTrackerBlockingDependencies()
}

override func presentedSubitemDidChange(at url: URL) {
guard url == store.fileUrl(for: .privacyConfiguration) else { return }
updateTrackerBlockingDependencies()
}
}
Loading
Loading