@@ -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
@@ -135,8 +163,30 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
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
+ 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
140
184
return . replace
141
185
}
142
186
}
187
+
188
+ enum SystemExtensionDelegateState {
189
+ case installing
190
+ case replacing
191
+ case deleting
192
+ }
0 commit comments