Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix: manually upgrade system extension#158

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 3 commits intomainfromethan/manually-upgrade-network-extension
May 16, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
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
3 changes: 2 additions & 1 deletionCoder-Desktop/Coder-Desktop/VPN/NetworkExtension.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,8 +58,9 @@ extension CoderVPNService {
try await tm.saveToPreferences()
neState = .disabled
} catch {
// This typically fails when the user declines the permission dialog
logger.error("save tunnel failed: \(error)")
neState = .failed(error.localizedDescription)
neState = .failed("Failed to save tunnel: \(error.localizedDescription). Try logging in and out again.")
}
}

Expand Down
158 changes: 108 additions & 50 deletionsCoder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,35 @@ enum SystemExtensionState: Equatable, Sendable {
}
}

let extensionBundle: Bundle = {
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This was previously a computed variable (the body was reran each time it was used), now it's just a lazy static constant. The body here is unchanged.

let extensionsDirectoryURL = URL(
fileURLWithPath: "Contents/Library/SystemExtensions",
relativeTo: Bundle.main.bundleURL
)
let extensionURLs: [URL]
do {
extensionURLs = try FileManager.default.contentsOfDirectory(at: extensionsDirectoryURL,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
} catch {
fatalError("Failed to get the contents of " +
"\(extensionsDirectoryURL.absoluteString): \(error.localizedDescription)")
}

// here we're just going to assume that there is only ever going to be one SystemExtension
// packaged up in the application bundle. If we ever need to ship multiple versions or have
// multiple extensions, we'll need to revisit this assumption.
guard let extensionURL = extensionURLs.first else {
fatalError("Failed to find any system extensions")
}

guard let extensionBundle = Bundle(url: extensionURL) else {
fatalError("Failed to create a bundle with URL \(extensionURL.absoluteString)")
}

return extensionBundle
}()

protocol SystemExtensionAsyncRecorder: Sendable {
func recordSystemExtensionState(_ state: SystemExtensionState) async
}
Expand All@@ -36,50 +65,9 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
}
}

var extensionBundle: Bundle {
let extensionsDirectoryURL = URL(
fileURLWithPath: "Contents/Library/SystemExtensions",
relativeTo: Bundle.main.bundleURL
)
let extensionURLs: [URL]
do {
extensionURLs = try FileManager.default.contentsOfDirectory(at: extensionsDirectoryURL,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
} catch {
fatalError("Failed to get the contents of " +
"\(extensionsDirectoryURL.absoluteString): \(error.localizedDescription)")
}

// here we're just going to assume that there is only ever going to be one SystemExtension
// packaged up in the application bundle. If we ever need to ship multiple versions or have
// multiple extensions, we'll need to revisit this assumption.
guard let extensionURL = extensionURLs.first else {
fatalError("Failed to find any system extensions")
}

guard let extensionBundle = Bundle(url: extensionURL) else {
fatalError("Failed to create a bundle with URL \(extensionURL.absoluteString)")
}

return extensionBundle
}

func installSystemExtension() {
logger.info("activating SystemExtension")
guard let bundleID = extensionBundle.bundleIdentifier else {
logger.error("Bundle has no identifier")
return
}
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: bundleID,
queue: .main
)
let delegate = SystemExtensionDelegate(asyncDelegate: self)
systemExtnDelegate = delegate
request.delegate = delegate
OSSystemExtensionManager.shared.submitRequest(request)
logger.info("submitted SystemExtension request with bundleID: \(bundleID)")
systemExtnDelegate = SystemExtensionDelegate(asyncDelegate: self)
systemExtnDelegate!.installSystemExtension()
}
}

Expand All@@ -90,13 +78,31 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
{
private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn-installer")
private var asyncDelegate: AsyncDelegate
// The `didFinishWithResult` function is called for both activation,
// deactivation, and replacement requests. The API provides no way to
// differentiate them. https://developer.apple.com/forums/thread/684021
// This tracks the last request type made, to handle them accordingly.
private var action: SystemExtensionDelegateAction = .none

init(asyncDelegate: AsyncDelegate) {
self.asyncDelegate = asyncDelegate
super.init()
logger.info("SystemExtensionDelegate initialized")
}

func installSystemExtension() {
logger.info("activating SystemExtension")
let bundleID = extensionBundle.bundleIdentifier!
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: bundleID,
queue: .main
)
request.delegate = self
action = .installing
OSSystemExtensionManager.shared.submitRequest(request)
logger.info("submitted SystemExtension request with bundleID: \(bundleID)")
}

func request(
_: OSSystemExtensionRequest,
didFinishWithResult result: OSSystemExtensionRequest.Result
Expand All@@ -109,24 +115,53 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
}
return
}
logger.info("SystemExtension activated")
Task { [asyncDelegate] in
await asyncDelegate.recordSystemExtensionState(SystemExtensionState.installed)
switch action {
case .installing:
logger.info("SystemExtension installed")
Task { [asyncDelegate] in
await asyncDelegate.recordSystemExtensionState(.installed)
}
action = .none
case .deleting:
logger.info("SystemExtension deleted")
Task { [asyncDelegate] in
await asyncDelegate.recordSystemExtensionState(.uninstalled)
}
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: extensionBundle.bundleIdentifier!,
queue: .main
)
request.delegate = self
action = .installing
OSSystemExtensionManager.shared.submitRequest(request)
case .replacing:
logger.info("SystemExtension replaced")
// The installed extension now has the same version strings as this
// bundle, so sending the deactivationRequest will work.
let request = OSSystemExtensionRequest.deactivationRequest(
forExtensionWithIdentifier: extensionBundle.bundleIdentifier!,
queue: .main
)
request.delegate = self
action = .deleting
OSSystemExtensionManager.shared.submitRequest(request)
case .none:
logger.warning("Received an unexpected request result")
}
}

func request(_: OSSystemExtensionRequest, didFailWithError error: Error) {
logger.error("System extension request failed: \(error.localizedDescription)")
Task { [asyncDelegate] in
await asyncDelegate.recordSystemExtensionState(
SystemExtensionState.failed(error.localizedDescription))
.failed(error.localizedDescription))
}
}

func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
logger.error("Extension \(request.identifier) requires user approval")
Task { [asyncDelegate] in
await asyncDelegate.recordSystemExtensionState(SystemExtensionState.needsUserApproval)
await asyncDelegate.recordSystemExtensionState(.needsUserApproval)
}
}

Expand All@@ -135,8 +170,31 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
actionForReplacingExtension existing: OSSystemExtensionProperties,
withExtension extension: OSSystemExtensionProperties
) -> OSSystemExtensionRequest.ReplacementAction {
// swiftlint:disable:next line_length
logger.info("Replacing \(request.identifier) v\(existing.bundleShortVersion) with v\(`extension`.bundleShortVersion)")
logger.info("Replacing \(request.identifier) v\(existing.bundleVersion) with v\(`extension`.bundleVersion)")
// This is counterintuitive, but this function is only called if the
// versions are the same in a dev environment.
// In a release build, this only gets called when the version string is
// different. We don't want to manually reinstall the extension in a dev
// environment, because the bug doesn't happen.
if existing.bundleVersion == `extension`.bundleVersion {
return .replace
}
// To work around the bug described in
// https://github.com/coder/coder-desktop-macos/issues/121,
// we're going to manually reinstall after the replacement is done.
// If we returned `.cancel` here the deactivation request will fail as
// it looks for an extension with the *current* version string.
// There's no way to modify the deactivate request to use a different
// version string (i.e. `existing.bundleVersion`).
logger.info("App upgrade detected, replacing and then reinstalling")
action = .replacing
return .replace
}
}

enum SystemExtensionDelegateAction {
case none
case installing
case replacing
case deleting
}
19 changes: 18 additions & 1 deletionCoder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,6 +81,21 @@ struct VPNMenu<VPN: VPNService, FS: FileSyncDaemon>: View {
}.buttonStyle(.plain)
TrayDivider()
}
// This shows when
// 1. The user is logged in
// 2. The network extension is installed
// 3. The VPN is unconfigured
// It's accompanied by a message in the VPNState view
// that the user needs to reconfigure.
if state.hasSession, vpn.state == .failed(.networkExtensionError(.unconfigured)) {
Button {
state.reconfigure()
} label: {
ButtonRowView {
Text("Reconfigure VPN")
}
}.buttonStyle(.plain)
}
if vpn.state == .failed(.systemExtensionError(.needsUserApproval)) {
Button {
openSystemExtensionSettings()
Expand DownExpand Up@@ -128,7 +143,9 @@ struct VPNMenu<VPN: VPNService, FS: FileSyncDaemon>: View {
vpn.state == .connecting ||
vpn.state == .disconnecting ||
// Prevent starting the VPN before the user has approved the system extension.
vpn.state == .failed(.systemExtensionError(.needsUserApproval))
vpn.state == .failed(.systemExtensionError(.needsUserApproval)) ||
// Prevent starting the VPN without a VPN configuration.
vpn.state == .failed(.networkExtensionError(.unconfigured))
}
}

Expand Down
6 changes: 5 additions & 1 deletionCoder-Desktop/Coder-Desktop/Views/VPN/VPNState.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,10 @@ struct VPNState<VPN: VPNService>: View {
Text("Sign in to use Coder Desktop")
.font(.body)
.foregroundColor(.secondary)
case (.failed(.networkExtensionError(.unconfigured)), _):
Text("The system VPN requires reconfiguration.")
.font(.body)
.foregroundStyle(.secondary)
case (.disabled, _):
Text("Enable Coder Connect to see workspaces")
.font(.body)
Expand All@@ -38,7 +42,7 @@ struct VPNState<VPN: VPNService>: View {
.padding(.horizontal, Theme.Size.trayInset)
.padding(.vertical, Theme.Size.trayPadding)
.frame(maxWidth: .infinity)
default:
case (.connected, true):
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Same behaviour, just more explicit.

EmptyView()
}
}
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp