Skip to content

Commit

Permalink
Merge release/1.94.0 into main
Browse files Browse the repository at this point in the history
  • Loading branch information
daxmobile authored Jun 28, 2024
2 parents 0753aa1 + a943395 commit 0bda737
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 67 deletions.
2 changes: 1 addition & 1 deletion Configuration/BuildNumber.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CURRENT_PROJECT_VERSION = 210
CURRENT_PROJECT_VERSION = 211
9 changes: 9 additions & 0 deletions DuckDuckGo/Waitlist/IPCServiceLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ final class IPCServiceLauncher {
self.launchMethod = launchMethod
}

func checkPrerequisites() -> Bool {
switch launchMethod {
case .direct(_, let appLauncher):
return appLauncher.targetAppExists()
case .loginItem:
return true
}
}

/// Enables the IPC service
///
func enable() async throws {
Expand Down
150 changes: 87 additions & 63 deletions DuckDuckGo/Waitlist/VPNUninstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ final class VPNUninstaller: VPNUninstalling {
}

enum IPCUninstallAttempt: PixelKitEventV2 {
case prevented
case begin
case cancelled(_ reason: UninstallCancellationReason)
case success
case failure(_ error: Error)

var name: String {
switch self {
case .prevented:
return "vpn_browser_uninstall_prevented_uds"

case .begin:
return "vpn_browser_uninstall_attempt_uds"

Expand All @@ -97,7 +101,8 @@ final class VPNUninstaller: VPNUninstalling {

var parameters: [String: String]? {
switch self {
case .begin,
case .prevented,
.begin,
.success,
.failure:
return nil
Expand All @@ -108,7 +113,8 @@ final class VPNUninstaller: VPNUninstalling {

var error: Error? {
switch self {
case .begin,
case .prevented,
.begin,
.cancelled,
.success:
return nil
Expand Down Expand Up @@ -165,82 +171,100 @@ final class VPNUninstaller: VPNUninstalling {
///
@MainActor
func uninstall(removeSystemExtension: Bool) async throws {
// We want to check service launcher pre-requisited before firing any pixel,
// because if our VPN menu app isn't available where we're expecting to find it
// we want to avoid adding noise to our uninstall attempts and instead fire
// a daily pixel telling us the app wasn't found.
guard ipcServiceLauncher.checkPrerequisites() else {
pixelKit?.fire(IPCUninstallAttempt.prevented, frequency: .daily)
return
}

pixelKit?.fire(IPCUninstallAttempt.begin, frequency: .dailyAndCount)

do {
// We can do this optimistically as it has little if any impact.
unpinNetworkProtection()
try await executeUninstallSequence(removeSystemExtension: removeSystemExtension)
pixelKit?.fire(IPCUninstallAttempt.success, frequency: .dailyAndCount)
} catch UninstallError.cancelled(let reason) {
pixelKit?.fire(IPCUninstallAttempt.cancelled(reason), frequency: .dailyAndCount)
} catch {
pixelKit?.fire(IPCUninstallAttempt.failure(error), frequency: .dailyAndCount)
}
}

guard !isDisabling else {
throw UninstallError.cancelled(reason: .alreadyUninstalling)
}
/// Uninstalls the VPN
///
/// Don't call this directly but instead call ``uninstall(removeSystemExtension:)`` as it checks preconditions
/// and fires pixels.
///
@MainActor
private func executeUninstallSequence(removeSystemExtension: Bool) async throws {
// We can do this optimistically as it has little if any impact.
unpinNetworkProtection()

guard vpnMenuLoginItem.status.isInstalled else {
throw UninstallError.cancelled(reason: .alreadyUninstalled)
}
guard !isDisabling else {
throw UninstallError.cancelled(reason: .alreadyUninstalling)
}

isDisabling = true
guard vpnMenuLoginItem.status.isInstalled else {
throw UninstallError.cancelled(reason: .alreadyUninstalled)
}

defer {
resetUserDefaults(uninstallSystemExtension: removeSystemExtension)
}
isDisabling = true

do {
try await ipcServiceLauncher.enable()
} catch {
throw UninstallError.runAgentError(error)
}
defer {
resetUserDefaults(uninstallSystemExtension: removeSystemExtension)
}

// Allow some time for the login items to fully launch
try await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC)

do {
if removeSystemExtension {
try await ipcClient.uninstall(.all)
} else {
try await ipcClient.uninstall(.configuration)
}
} catch {
print("Failed to uninstall VPN, with error: \(error.localizedDescription)")

switch error {
case OSSystemExtensionError.requestCanceled:
throw UninstallError.cancelled(reason: .sysexInstallationCancelled)
case OSSystemExtensionError.authorizationRequired:
throw UninstallError.cancelled(reason: .sysexInstallationRequiresAuthorization)
default:
throw UninstallError.uninstallError(error)
}
}
do {
try await ipcServiceLauncher.enable()
} catch {
throw UninstallError.runAgentError(error)
}

// Allow some time for the login items to fully launch
try await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC)

// We want to give some time for the login item to reset state before disabling it
try? await Task.sleep(interval: 0.5)
do {
if removeSystemExtension {
try await ipcClient.uninstall(.all)
} else {
try await ipcClient.uninstall(.configuration)
}
} catch {
print("Failed to uninstall VPN, with error: \(error.localizedDescription)")

switch error {
case OSSystemExtensionError.requestCanceled:
throw UninstallError.cancelled(reason: .sysexInstallationCancelled)
case OSSystemExtensionError.authorizationRequired:
throw UninstallError.cancelled(reason: .sysexInstallationRequiresAuthorization)
default:
throw UninstallError.uninstallError(error)
}
}

// Workaround: since status updates are provided through XPC we want to make sure the
// VPN is marked as disconnected. We may be able to more properly resolve this by using
// UDS for all VPN status updates.
//
// Ref: https://app.asana.com/0/0/1207499177312396/1207538373572594/f
//
VPNControllerXPCClient.shared.forceStatusToDisconnected()
// We want to give some time for the login item to reset state before disabling it
try? await Task.sleep(interval: 0.5)

// When the agent is registered as a login item, we want to unregister it
// and stop it from running, which is achieved by the next call.
removeAgents()
// Workaround: since status updates are provided through XPC we want to make sure the
// VPN is marked as disconnected. We may be able to more properly resolve this by using
// UDS for all VPN status updates.
//
// Ref: https://app.asana.com/0/0/1207499177312396/1207538373572594/f
//
VPNControllerXPCClient.shared.forceStatusToDisconnected()

// When the agent was started directly (not as a login item) we want to stop it,
// as the above call won't do anything for it.
try await stopAgents()
// When the agent is registered as a login item, we want to unregister it
// and stop it from running, which is achieved by the next call.
removeAgents()

notifyVPNUninstalled()
isDisabling = false
// When the agent was started directly (not as a login item) we want to stop it,
// as the above call won't do anything for it.
try await stopAgents()

pixelKit?.fire(IPCUninstallAttempt.success, frequency: .dailyAndCount)
} catch UninstallError.cancelled(let reason) {
pixelKit?.fire(IPCUninstallAttempt.cancelled(reason), frequency: .dailyAndCount)
} catch {
pixelKit?.fire(IPCUninstallAttempt.failure(error), frequency: .dailyAndCount)
}
notifyVPNUninstalled()
isDisabling = false
}

// Stop the VPN agents.
Expand Down
16 changes: 13 additions & 3 deletions LocalPackages/AppLauncher/Sources/AppLauncher/AppLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@ public final class AppLauncher: AppLaunching {
}

private let mainBundleURL: URL
private var workspace: NSWorkspace
private var fileManager: FileManager

public init(appBundleURL: URL) {
public init(appBundleURL: URL,
workspace: NSWorkspace = .shared,
fileManager: FileManager = .default) {
mainBundleURL = appBundleURL
self.workspace = workspace
self.fileManager = fileManager
}

public func launchApp(withCommand command: AppLaunchCommand) async throws {
Expand Down Expand Up @@ -76,12 +82,16 @@ public final class AppLauncher: AppLaunching {

do {
if let launchURL = command.launchURL {
return try await NSWorkspace.shared.open([launchURL], withApplicationAt: mainBundleURL, configuration: configuration)
return try await workspace.open([launchURL], withApplicationAt: mainBundleURL, configuration: configuration)
} else {
return try await NSWorkspace.shared.openApplication(at: mainBundleURL, configuration: configuration)
return try await workspace.openApplication(at: mainBundleURL, configuration: configuration)
}
} catch {
throw AppLaunchError.workspaceOpenError(error)
}
}

public func targetAppExists() -> Bool {
fileManager.fileExists(atPath: mainBundleURL.path)
}
}

0 comments on commit 0bda737

Please sign in to comment.