@@ -22,6 +22,35 @@ enum SystemExtensionState: Equatable, Sendable {
2222 }
2323}
2424
25+ let extensionBundle : Bundle = {
26+ let extensionsDirectoryURL = URL (
27+ fileURLWithPath: " Contents/Library/SystemExtensions " ,
28+ relativeTo: Bundle . main. bundleURL
29+ )
30+ let extensionURLs : [ URL ]
31+ do {
32+ extensionURLs = try FileManager . default. contentsOfDirectory ( at: extensionsDirectoryURL,
33+ includingPropertiesForKeys: nil ,
34+ options: . skipsHiddenFiles)
35+ } catch {
36+ fatalError ( " Failed to get the contents of " +
37+ " \( extensionsDirectoryURL. absoluteString) : \( error. localizedDescription) " )
38+ }
39+
40+ // here we're just going to assume that there is only ever going to be one SystemExtension
41+ // packaged up in the application bundle. If we ever need to ship multiple versions or have
42+ // multiple extensions, we'll need to revisit this assumption.
43+ guard let extensionURL = extensionURLs. first else {
44+ fatalError ( " Failed to find any system extensions " )
45+ }
46+
47+ guard let extensionBundle = Bundle ( url: extensionURL) else {
48+ fatalError ( " Failed to create a bundle with URL \( extensionURL. absoluteString) " )
49+ }
50+
51+ return extensionBundle
52+ } ( )
53+
2554protocol SystemExtensionAsyncRecorder : Sendable {
2655 func recordSystemExtensionState( _ state: SystemExtensionState ) async
2756}
@@ -36,35 +65,6 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
3665 }
3766 }
3867
39- var extensionBundle : Bundle {
40- let extensionsDirectoryURL = URL (
41- fileURLWithPath: " Contents/Library/SystemExtensions " ,
42- relativeTo: Bundle . main. bundleURL
43- )
44- let extensionURLs : [ URL ]
45- do {
46- extensionURLs = try FileManager . default. contentsOfDirectory ( at: extensionsDirectoryURL,
47- includingPropertiesForKeys: nil ,
48- options: . skipsHiddenFiles)
49- } catch {
50- fatalError ( " Failed to get the contents of " +
51- " \( extensionsDirectoryURL. absoluteString) : \( error. localizedDescription) " )
52- }
53-
54- // here we're just going to assume that there is only ever going to be one SystemExtension
55- // packaged up in the application bundle. If we ever need to ship multiple versions or have
56- // multiple extensions, we'll need to revisit this assumption.
57- guard let extensionURL = extensionURLs. first else {
58- fatalError ( " Failed to find any system extensions " )
59- }
60-
61- guard let extensionBundle = Bundle ( url: extensionURL) else {
62- fatalError ( " Failed to create a bundle with URL \( extensionURL. absoluteString) " )
63- }
64-
65- return extensionBundle
66- }
67-
6868 func installSystemExtension( ) {
6969 logger. info ( " activating SystemExtension " )
7070 guard let bundleID = extensionBundle. bundleIdentifier else {
@@ -75,9 +75,7 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
7575 forExtensionWithIdentifier: bundleID,
7676 queue: . main
7777 )
78- let delegate = SystemExtensionDelegate ( asyncDelegate: self )
79- systemExtnDelegate = delegate
80- request. delegate = delegate
78+ request. delegate = systemExtnDelegate
8179 OSSystemExtensionManager . shared. submitRequest ( request)
8280 logger. info ( " submitted SystemExtension request with bundleID: \( bundleID) " )
8381 }
@@ -90,6 +88,10 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
9088{
9189 private var logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " vpn-installer " )
9290 private var asyncDelegate : AsyncDelegate
91+ // The `didFinishWithResult` function is called for both activation,
92+ // deactivation, and replacement requests. The API provides no way to
93+ // differentiate them. https://developer.apple.com/forums/thread/684021
94+ private var state : SystemExtensionDelegateState = . installing
9395
9496 init ( asyncDelegate: AsyncDelegate ) {
9597 self . asyncDelegate = asyncDelegate
@@ -109,9 +111,35 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
109111 }
110112 return
111113 }
112- logger. info ( " SystemExtension activated " )
113- Task { [ asyncDelegate] in
114- await asyncDelegate. recordSystemExtensionState ( SystemExtensionState . installed)
114+ switch state {
115+ case . installing:
116+ logger. info ( " SystemExtension installed " )
117+ Task { [ asyncDelegate] in
118+ await asyncDelegate. recordSystemExtensionState ( SystemExtensionState . installed)
119+ }
120+ case . deleting:
121+ logger. info ( " SystemExtension deleted " )
122+ Task { [ asyncDelegate] in
123+ await asyncDelegate. recordSystemExtensionState ( SystemExtensionState . uninstalled)
124+ }
125+ let request = OSSystemExtensionRequest . activationRequest (
126+ forExtensionWithIdentifier: extensionBundle. bundleIdentifier!,
127+ queue: . main
128+ )
129+ request. delegate = self
130+ state = . installing
131+ OSSystemExtensionManager . shared. submitRequest ( request)
132+ case . replacing:
133+ logger. info ( " SystemExtension replaced " )
134+ // The installed extension now has the same version strings as this
135+ // bundle, so sending the deactivationRequest will work.
136+ let request = OSSystemExtensionRequest . deactivationRequest (
137+ forExtensionWithIdentifier: extensionBundle. bundleIdentifier!,
138+ queue: . main
139+ )
140+ request. delegate = self
141+ state = . deleting
142+ OSSystemExtensionManager . shared. submitRequest ( request)
115143 }
116144 }
117145
@@ -131,12 +159,32 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
131159 }
132160
133161 func request(
134- _ request : OSSystemExtensionRequest ,
162+ _: OSSystemExtensionRequest ,
135163 actionForReplacingExtension existing: OSSystemExtensionProperties ,
136164 withExtension extension: OSSystemExtensionProperties
137165 ) -> OSSystemExtensionRequest . ReplacementAction {
138- // swiftlint:disable:next line_length
139- logger. info ( " Replacing \( request. identifier) v \( existing. bundleShortVersion) with v \( `extension`. bundleShortVersion) " )
166+ // This is counterintuitive, but this function is only called if the
167+ // versions are the same in a dev environment.
168+ // In a release build, this only gets called when the version string is
169+ // different. We don't want to manually reinstall the extension in a dev
170+ // environment, because the bug doesn't happen.
171+ if existing. bundleVersion == `extension`. bundleVersion {
172+ return . replace
173+ }
174+ // To work around the bug described in
175+ // https://github.com/coder/coder-desktop-macos/issues/121,
176+ // we're going to manually reinstall after the replacement is done.
177+ // If we returned `.cancel` here the deactivation request will fail as
178+ // it looks for an extension with the *current* version string.
179+ // There's no way to modify the deactivate request to use a different
180+ // version string (i.e. `existing.bundleVersion`).
181+ state = . replacing
140182 return . replace
141183 }
142184}
185+
186+ enum SystemExtensionDelegateState {
187+ case installing
188+ case replacing
189+ case deleting
190+ }
0 commit comments