@@ -22,6 +22,35 @@ enum SystemExtensionState: Equatable, Sendable {
22
22
}
23
23
}
24
24
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
+
25
54
protocol SystemExtensionAsyncRecorder : Sendable {
26
55
func recordSystemExtensionState( _ state: SystemExtensionState ) async
27
56
}
@@ -36,35 +65,6 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
36
65
}
37
66
}
38
67
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
-
68
68
func installSystemExtension( ) {
69
69
logger. info ( " activating SystemExtension " )
70
70
guard let bundleID = extensionBundle. bundleIdentifier else {
@@ -75,9 +75,7 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
75
75
forExtensionWithIdentifier: bundleID,
76
76
queue: . main
77
77
)
78
- let delegate = SystemExtensionDelegate ( asyncDelegate: self )
79
- systemExtnDelegate = delegate
80
- request. delegate = delegate
78
+ request. delegate = systemExtnDelegate
81
79
OSSystemExtensionManager . shared. submitRequest ( request)
82
80
logger. info ( " submitted SystemExtension request with bundleID: \( bundleID) " )
83
81
}
@@ -90,6 +88,10 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
90
88
{
91
89
private var logger = Logger ( subsystem: Bundle . main. bundleIdentifier!, category: " vpn-installer " )
92
90
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
93
95
94
96
init ( asyncDelegate: AsyncDelegate ) {
95
97
self . asyncDelegate = asyncDelegate
@@ -109,9 +111,35 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
109
111
}
110
112
return
111
113
}
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)
115
143
}
116
144
}
117
145
@@ -131,12 +159,32 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
131
159
}
132
160
133
161
func request(
134
- _ request : OSSystemExtensionRequest ,
162
+ _: OSSystemExtensionRequest ,
135
163
actionForReplacingExtension existing: OSSystemExtensionProperties ,
136
164
withExtension extension: OSSystemExtensionProperties
137
165
) -> 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
140
182
return . replace
141
183
}
142
184
}
185
+
186
+ enum SystemExtensionDelegateState {
187
+ case installing
188
+ case replacing
189
+ case deleting
190
+ }
0 commit comments