@@ -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 and
92+ // deactivation requests and the API provides no way to differentiate them.
93+ // https://developer.apple.com/forums/thread/684021
94+ private var isReinstalling = false
9395
9496 init ( asyncDelegate: AsyncDelegate ) {
9597 self . asyncDelegate = asyncDelegate
@@ -109,9 +111,23 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
109111 }
110112 return
111113 }
112- logger. info ( " SystemExtension activated " )
113- Task { [ asyncDelegate] in
114- await asyncDelegate. recordSystemExtensionState ( SystemExtensionState . installed)
114+ if isReinstalling {
115+ logger. info ( " SystemExtension deleted " )
116+ Task { [ asyncDelegate] in
117+ await asyncDelegate. recordSystemExtensionState ( SystemExtensionState . uninstalled)
118+ }
119+ let request = OSSystemExtensionRequest . activationRequest (
120+ forExtensionWithIdentifier: extensionBundle. bundleIdentifier!,
121+ queue: . main
122+ )
123+ request. delegate = self
124+ isReinstalling = false
125+ OSSystemExtensionManager . shared. submitRequest ( request)
126+ } else {
127+ logger. info ( " SystemExtension installed " )
128+ Task { [ asyncDelegate] in
129+ await asyncDelegate. recordSystemExtensionState ( SystemExtensionState . installed)
130+ }
115131 }
116132 }
117133
@@ -131,12 +147,27 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
131147 }
132148
133149 func request(
134- _ request : OSSystemExtensionRequest ,
150+ _: OSSystemExtensionRequest ,
135151 actionForReplacingExtension existing: OSSystemExtensionProperties ,
136152 withExtension extension: OSSystemExtensionProperties
137153 ) -> OSSystemExtensionRequest . ReplacementAction {
138- // swiftlint:disable:next line_length
139- logger. info ( " Replacing \( request. identifier) v \( existing. bundleShortVersion) with v \( `extension`. bundleShortVersion) " )
140- return . replace
154+ // This is counterintuitive, but this function is only called if the versions are the same in a dev environment.
155+ // In a release build, this only gets called when the version string is different.
156+ // We don't want to manually reinstall the extension in a dev environment, because the bug doesn't happen.
157+ if existing. bundleVersion == `extension`. bundleVersion {
158+ return . replace
159+ }
160+ // To work around the bug described in https://github.com/coder/coder-desktop-macos/issues/121,
161+ // we're not going to return .replace, instead we're going to manually reinstall the extension.
162+ defer {
163+ let request = OSSystemExtensionRequest . deactivationRequest (
164+ forExtensionWithIdentifier: extensionBundle. bundleIdentifier!,
165+ queue: . main
166+ )
167+ request. delegate = self
168+ isReinstalling = true
169+ OSSystemExtensionManager . shared. submitRequest ( request)
170+ }
171+ return . cancel
141172 }
142173}
0 commit comments