Skip to content

Commit

Permalink
Make remote config accessible to background agents (#947)
Browse files Browse the repository at this point in the history
<!--
Note: This checklist is a reminder of our shared engineering
expectations.
-->

Please review the release process for BrowserServicesKit
[here](https://app.asana.com/0/1200194497630846/1200837094583426).

**Required**:

Task/Issue URL:
https://app.asana.com/0/1203581873609357/1207165680693234/f
iOS PR: duckduckgo/iOS#3255
macOS PR: duckduckgo/macos-browser#3124
What kind of version bump will this require?: Major
**Optional**:

Tech Design URL:
CC:

**Description**:
This PR modularizes the config to allow it to be used in background
processes

<!--
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.
-->

**Steps to test this PR**:
1. Run the VPN and DBP background agents
2. Look for `Configuration` logs that indicate the config being
downloaded or refreshed
3. If the config is setup correctly you can watch for the pixel test as
well.
1.

<!--
Before submitting a PR, please ensure you have tested the combinations
you expect the reviewer to test, then delete configurations you *know*
do not need explicit testing.

Using a simulator where a physical device is unavailable is acceptable.
-->

**OS Testing**:

* [ ] iOS 14
* [ ] iOS 15
* [ ] iOS 16
* [ ] macOS 10.15
* [ ] macOS 11
* [ ] macOS 12

---
###### Internal references:
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
  • Loading branch information
SlayterDev authored Sep 17, 2024
1 parent ae3dbec commit f7083a3
Show file tree
Hide file tree
Showing 9 changed files with 520 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PhishingDetection"
BuildableName = "PhishingDetection"
BlueprintName = "PhishingDetection"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand Down Expand Up @@ -722,6 +736,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PhishingDetectionTests"
BuildableName = "PhishingDetectionTests"
BlueprintName = "PhishingDetectionTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public enum PrivacyFeature: String {
case brokenSitePrompt
case remoteMessaging
case additionalCampaignPixelParams
case backgroundAgentPixelTest
case newTabPageImprovements
case syncPromotion
case autofillSurveys
Expand Down Expand Up @@ -139,6 +140,11 @@ public enum DuckPlayerSubfeature: String, PrivacySubfeature {
case enableDuckPlayer // iOS DuckPlayer rollout feature
}

public enum BackgroundAgentPixelTestSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature { .backgroundAgentPixelTest }
case pixelTest
}

public enum PhishingDetectionSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature { .phishingDetection }
case allowErrorPage
Expand Down
2 changes: 1 addition & 1 deletion Sources/Configuration/ConfigurationFetching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Foundation
import Common
import Networking

protocol ConfigurationFetching {
public protocol ConfigurationFetching {

func fetch(_ configuration: Configuration, isDebug: Bool) async throws
func fetch(all configurations: [Configuration]) async throws
Expand Down
2 changes: 2 additions & 0 deletions Sources/Configuration/ConfigurationStoring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public protocol ConfigurationStoring {
mutating func saveData(_ data: Data, for configuration: Configuration) throws
mutating func saveEtag(_ etag: String, for configuration: Configuration) throws

func fileUrl(for configuration: Configuration) -> URL

}
164 changes: 164 additions & 0 deletions Sources/Configuration/DefaultConfigurationManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// DefaultConfigurationManager.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import os.log
import Combine
import Common
import Persistence

public extension Logger {
static var config: Logger = { Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Configuration") }()
}

open class DefaultConfigurationManager: NSObject {
public enum Error: Swift.Error {

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

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

}

public enum Constants {

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

static let lastUpdateDefaultsKey = "configuration.lastUpdateTime"
}

private var defaults: KeyValueStoring

public var fetcher: ConfigurationFetching
public var store: ConfigurationStoring

public init(fetcher: ConfigurationFetching, store: ConfigurationStoring, defaults: KeyValueStoring) {
self.fetcher = fetcher
self.store = store
self.defaults = defaults
super.init()
NSFileCoordinator.addFilePresenter(self)
}

deinit {
NSFileCoordinator.removeFilePresenter(self)
}

public static let queue: DispatchQueue = DispatchQueue(label: "Configuration Manager")
public static let filePresenterOperationQueue = OperationQueue()

public var lastUpdateTime: Date {
get {
defaults.object(forKey: Constants.lastUpdateDefaultsKey) as? Date ?? .distantPast
}
set {
defaults.set(newValue, forKey: Constants.lastUpdateDefaultsKey)
}
}

private var timerCancellable: AnyCancellable?
private var refreshTask: Task<Never, Swift.Error>? {
willSet {
refreshTask?.cancel()
}
}
public var lastRefreshCheckTime: Date = Date()

public func start() {
Logger.config.debug("Starting configuration refresh timer")
refreshTask = Task.periodic(interval: Constants.refreshCheckIntervalSeconds) {
Self.queue.async { [weak self] in
self?.lastRefreshCheckTime = Date()
self?.refreshIfNeeded()
}
}
Task {
await refreshNow()
}
}

/// Implement this in the subclass.
/// Use this method to fetch neccessary configurations and store them.
open func refreshNow(isDebug: Bool = false) async {
fatalError("refreshNow Must be implemented by subclass")
}

@discardableResult
private 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()
}
}

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

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

/// Will try to update the config again at the regularly scheduled interval
/// **Note:** You must call `start()` on your `ConfigurationManager` instance for this to take effect. It relies on the internal refresh loop of the
/// `DefaultConfigurationManager` class
public func tryAgainLater() {
lastUpdateTime = Date()
}

/// Will try to update the config again after `Constants.retryDelaySeconds`
/// **Note:** You must call `start()` on your `ConfigurationManager` instance for this to take effect. It relies on the internal refresh loop of the
/// `DefaultConfigurationManager` class
public func tryAgainSoon() {
// Set the last update time to in the past so it triggers again sooner
lastUpdateTime = Date(timeIntervalSinceNow: Constants.refreshPeriodSeconds - Constants.retryDelaySeconds)
}
}

extension DefaultConfigurationManager: NSFilePresenter {
open var presentedItemURL: URL? {
return nil
}

public var presentedItemOperationQueue: OperationQueue {
return Self.filePresenterOperationQueue
}

open func presentedSubitemDidChange(at url: URL) { }

open func presentedSubitemDidAppear(at url: URL) { }

}
4 changes: 4 additions & 0 deletions Sources/TestUtils/MockKeyValueStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public class MockKeyValueStore: KeyValueStoring {
store[defaultName] = nil
}

public func clearAll() {
store.removeAll()
}

}

extension MockKeyValueStore: DictionaryRepresentable {
Expand Down
Loading

0 comments on commit f7083a3

Please sign in to comment.