@@ -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
@@ -135,8 +163,30 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
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+ logger. info ( " Replacing \( request. identifier) v \( existing. bundleVersion) with v \( `extension`. bundleVersion) " )
167+ // This is counterintuitive, but this function is only called if the
168+ // versions are the same in a dev environment.
169+ // In a release build, this only gets called when the version string is
170+ // different. We don't want to manually reinstall the extension in a dev
171+ // environment, because the bug doesn't happen.
172+ if existing. bundleVersion == `extension`. bundleVersion {
173+ return . replace
174+ }
175+ // To work around the bug described in
176+ // https://github.com/coder/coder-desktop-macos/issues/121,
177+ // we're going to manually reinstall after the replacement is done.
178+ // If we returned `.cancel` here the deactivation request will fail as
179+ // it looks for an extension with the *current* version string.
180+ // There's no way to modify the deactivate request to use a different
181+ // version string (i.e. `existing.bundleVersion`).
182+ logger. info ( " App upgrade detected, replacing and then reinstalling " )
183+ state = . replacing
140184 return . replace
141185 }
142186}
187+
188+ enum SystemExtensionDelegateState {
189+ case installing
190+ case replacing
191+ case deleting
192+ }
0 commit comments