@@ -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 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
93
95
94
96
init ( asyncDelegate: AsyncDelegate ) {
95
97
self . asyncDelegate = asyncDelegate
@@ -109,9 +111,23 @@ 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
+ 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
+ }
115
131
}
116
132
}
117
133
@@ -131,12 +147,27 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
131
147
}
132
148
133
149
func request(
134
- _ request : OSSystemExtensionRequest ,
150
+ _: OSSystemExtensionRequest ,
135
151
actionForReplacingExtension existing: OSSystemExtensionProperties ,
136
152
withExtension extension: OSSystemExtensionProperties
137
153
) -> 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
141
172
}
142
173
}
0 commit comments