Skip to content

Commit

Permalink
Make remote config accessible to background agents (#3124)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1203581873609357/1207165680693234/f
Tech Design URL:
CC:

**Description**:

**Steps to test this PR**:
1. See duckduckgo/BrowserServicesKit#947

<!--
Tagging instructions
If this PR isn't ready to be merged for whatever reason it should be
marked with the `DO NOT MERGE` label (particularly if it's a draft)
If it's pending Product Review/PFR, please add the `Pending Product
Review` label.

If at any point it isn't actively being worked on/ready for
review/otherwise moving forward (besides the above PR/PFR exception)
strongly consider closing it (or not opening it in the first place). If
you decide not to close it, make sure it's labelled to make it clear the
PRs state and comment with more information.
-->

**Definition of Done**:

* [ ] Does this PR satisfy our [Definition of
Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)?

---
###### Internal references:
[Pull Request Review
Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f)
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
[Pull Request
Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f)
  • Loading branch information
SlayterDev authored Sep 17, 2024
1 parent 9ef86c6 commit 9cc7e9c
Show file tree
Hide file tree
Showing 43 changed files with 1,437 additions and 278 deletions.
156 changes: 155 additions & 1 deletion 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,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
"branch" : "196.0.0",
"revision" : "ae3dbec01b8b72dc2ea4c510aecbc802862eab63"
"revision" : "f7083a3c74a4aa1f6a0f4ab65265eb2f422a2cf0",
"version" : "196.1.0"
}
},
{
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

0 comments on commit 9cc7e9c

Please sign in to comment.