Skip to content

Commit

Permalink
Adds IPC start pixels for the VPN
Browse files Browse the repository at this point in the history
  • Loading branch information
diegoreymendez committed Apr 15, 2024
1 parent 99aa075 commit c721f8f
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,39 @@ final class NetworkProtectionIPCTunnelController {
private let featureVisibility: NetworkProtectionFeatureVisibility
private let loginItemsManager: LoginItemsManaging
private let ipcClient: NetworkProtectionIPCClient
private let pixelKit: PixelFiring?

init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(),
loginItemsManager: LoginItemsManaging = LoginItemsManager(),
ipcClient: NetworkProtectionIPCClient) {
ipcClient: NetworkProtectionIPCClient,
pixelKit: PixelFiring? = PixelKit.shared) {

self.featureVisibility = featureVisibility
self.loginItemsManager = loginItemsManager
self.ipcClient = ipcClient
self.pixelKit = pixelKit
}

// MARK: - Login Items Manager

private func enableLoginItems() async throws -> Bool {
guard try await featureVisibility.canStartVPN() else {
// We shouldn't enable the menu app is the VPN feature is disabled.
return false
}

try loginItemsManager.throwingEnableLoginItems(LoginItemsManager.networkProtectionLoginItems, log: .networkProtection)
return true
}
}

// MARK: - TunnelController Conformance

extension NetworkProtectionIPCTunnelController: TunnelController {

@MainActor
func start(attemptHandler: VPNStartAttemptHandling = DefaultVPNStartAttemptHandler()) async {
attemptHandler.begin()
func start() async {
try? await pixelKit?.fire(StartAttempt.begin)

Check warning on line 64 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Pro)

result of 'try?' is unused

Check warning on line 64 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Browser)

result of 'try?' is unused

Check warning on line 64 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Non-Sandbox)

result of 'try?' is unused

Check warning on line 64 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Sandbox)

result of 'try?' is unused

do {
guard try await enableLoginItems() else {
Expand All @@ -50,16 +70,16 @@ final class NetworkProtectionIPCTunnelController {
}

ipcClient.start()
attemptHandler.success()
try? await pixelKit?.fire(StartAttempt.success, frequency: .dailyAndContinuous)

Check warning on line 73 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Pro)

result of 'try?' is unused

Check warning on line 73 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Browser)

result of 'try?' is unused

Check warning on line 73 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Non-Sandbox)

result of 'try?' is unused

Check warning on line 73 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Sandbox)

result of 'try?' is unused
} catch {
os_log("🔴 IPC Controller found en error when starting the VPN: \(error)", log: .networkProtection)
attemptHandler.failure(error)
try? await pixelKit?.fire(StartAttempt.failure(error), frequency: .dailyAndContinuous)

Check warning on line 76 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Pro)

result of 'try?' is unused

Check warning on line 76 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Browser)

result of 'try?' is unused

Check warning on line 76 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Non-Sandbox)

result of 'try?' is unused

Check warning on line 76 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Sandbox)

result of 'try?' is unused
}
}

@MainActor
func stop(attemptHandler: VPNStopAttemptHandling) async {
attemptHandler.begin()
func stop() async {
try? await pixelKit?.fire(StopAttempt.begin)

Check warning on line 82 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Sandbox)

result of 'try?' is unused

do {
guard try await enableLoginItems() else {
Expand All @@ -68,40 +88,13 @@ final class NetworkProtectionIPCTunnelController {
}

ipcClient.stop()
attemptHandler.success()
try? await pixelKit?.fire(StopAttempt.success, frequency: .dailyAndContinuous)

Check warning on line 91 in DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift

View workflow job for this annotation

GitHub Actions / Test (Sandbox)

result of 'try?' is unused
} catch {
os_log("🔴 IPC Controller found en error when starting the VPN: \(error)", log: .networkProtection)
attemptHandler.failure(error)
try? await pixelKit?.fire(StopAttempt.failure(error), frequency: .dailyAndContinuous)
}
}

// MARK: - Login Items Manager

private func enableLoginItems() async throws -> Bool {
guard try await featureVisibility.canStartVPN() else {
// We shouldn't enable the menu app is the VPN feature is disabled.
return false
}

try loginItemsManager.throwingEnableLoginItems(LoginItemsManager.networkProtectionLoginItems, log: .networkProtection)
return true
}
}

// MARK: - TunnelController Conformance

extension NetworkProtectionIPCTunnelController: TunnelController {

@MainActor
func start() async {
await start(attemptHandler: DefaultVPNStartAttemptHandler())
}

@MainActor
func stop() async {
await stop(attemptHandler: DefaultVPNStopAttemptHandler())
}

/// Queries VPN to know if it's connected.
///
/// - Returns: `true` if the VPN is connected, connecting or reasserting, and `false` otherwise.
Expand All @@ -119,15 +112,9 @@ extension NetworkProtectionIPCTunnelController: TunnelController {

// MARK: - Start Attempts

protocol VPNStartAttemptHandling {
func begin()
func success()
func failure(_ error: Error)
}

extension NetworkProtectionIPCTunnelController {

private enum StartAttempt: PixelKitEventV2 {
enum StartAttempt: PixelKitEventV2 {
case begin
case success
case failure(_ error: Error)
Expand Down Expand Up @@ -159,39 +146,13 @@ extension NetworkProtectionIPCTunnelController {
}
}
}

private class DefaultVPNStartAttemptHandler: VPNStartAttemptHandling {
private let pixelKit: PixelKit?

init(pixelKit: PixelKit? = .shared) {
self.pixelKit = pixelKit
}

func begin() {
pixelKit?.fire(StartAttempt.begin)
}

func success() {
pixelKit?.fire(StartAttempt.success, frequency: .dailyAndContinuous)
}

func failure(_ error: Error) {
pixelKit?.fire(StartAttempt.failure(error), frequency: .dailyAndContinuous)
}
}
}

// MARK: - Stop Attempts

protocol VPNStopAttemptHandling {
func begin()
func success()
func failure(_ error: Error)
}

extension NetworkProtectionIPCTunnelController {

private enum StopAttempt: PixelKitEventV2 {
enum StopAttempt: PixelKitEventV2 {
case begin
case success
case failure(_ error: Error)
Expand Down Expand Up @@ -223,24 +184,4 @@ extension NetworkProtectionIPCTunnelController {
}
}
}

private class DefaultVPNStopAttemptHandler: VPNStopAttemptHandling {
private let pixelKit: PixelKit?

init(pixelKit: PixelKit? = .shared) {
self.pixelKit = pixelKit
}

func begin() {
pixelKit?.fire(StopAttempt.begin)
}

func success() {
pixelKit?.fire(StopAttempt.success, frequency: .dailyAndContinuous)
}

func failure(_ error: Error) {
pixelKit?.fire(StopAttempt.failure(error), frequency: .dailyAndContinuous)
}
}
}
39 changes: 39 additions & 0 deletions LocalPackages/PixelKit/Sources/PixelKit/PixelKitEventV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,42 @@ import Foundation
public protocol PixelKitEventV2: PixelKitEvent {
var error: Error? { get }
}

/// Protocol to support mocking pixel firing.
///
/// We're adding support for `PixelKitEventV2` events strategically because adding support for earlier pixels
/// would be more complicated and time consuming. The idea of V2 events is that fire calls should not include a lot
/// of parameters. Parameters should be provided by the `PixelKitEventV2` protocol (extending it if necessary)
/// and the call to `fire` should process those properties to serialize in the requests.
///
public protocol PixelFiring {
@discardableResult
func fire(_ event: PixelKitEventV2) async throws -> Bool

@discardableResult
func fire(_ event: PixelKitEventV2,
frequency: PixelKit.Frequency) async throws -> Bool
}


extension PixelKit: PixelFiring {
@discardableResult
public func fire(_ event: PixelKitEventV2) async throws -> Bool {
try await fire(event, frequency: .standard)
}

@discardableResult
public func fire(_ event: PixelKitEventV2,
frequency: PixelKit.Frequency) async throws -> Bool {
try await withCheckedThrowingContinuation { continuation in
fire(event, frequency: frequency) { fired, error in
if let error {
continuation.resume(throwing: error)
return
}

continuation.resume(returning: fired)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//

Check failure on line 1 in LocalPackages/PixelKit/Sources/PixelKitTestingUtilities/PixelKitMock.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Header comments should be consistent with project patterns (file_header)
// PixelFireMock.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 PixelKit

public final class PixelKitMock: PixelFiring {

/// An array of fire calls, in order, that this mock expects
///
private let expectedFireCalls: [ExpectedFireCall]

/// The actual fire calls
///
private var actualFireCalls = [ExpectedFireCall]()

public init(expecting expectedFireCalls: [ExpectedFireCall]) {
self.expectedFireCalls = expectedFireCalls
}

public func fire(_ event: PixelKitEventV2) async throws -> Bool {
try await fire(event, frequency: .standard)
}

public func fire(_ event: PixelKitEventV2, frequency: PixelKit.Frequency) async throws -> Bool {
let fireCall = ExpectedFireCall(pixel: event, frequency: frequency)
actualFireCalls.append(fireCall)
return true
}

public var expectationsMet: Bool {
expectedFireCalls == actualFireCalls
}
}

public struct ExpectedFireCall: Equatable {
let pixel: PixelKitEventV2
let frequency: PixelKit.Frequency

public init(pixel: PixelKitEventV2, frequency: PixelKit.Frequency) {
self.pixel = pixel
self.frequency = frequency
}

public static func == (lhs: ExpectedFireCall, rhs: ExpectedFireCall) -> Bool {
lhs.pixel.name == rhs.pixel.name
&& lhs.pixel.parameters == rhs.pixel.parameters
&& (lhs.pixel.error as? NSError) == (rhs.pixel.error as? NSError)
&& lhs.frequency == rhs.frequency
}
}
Loading

0 comments on commit c721f8f

Please sign in to comment.