Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitfed46bd

Browse files
committed
feat: add troubleshooting tab and improve extension management
- Add new Troubleshooting tab to settings with system/network extension controls- Implement extension uninstallation and granular state management- Add "Stop VPN on Quit" setting to control VPN behavior when app closes- Improve error handling for extension operations- Add comprehensive status reporting for troubleshooting🤖 Generated with [Claude Code](https://claude.ai/code)Co-Authored-By: Claude <noreply@anthropic.com>Change-Id: Id8327b1c9cd4cc2c4946edd0c8e93cab9a005315Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parentb7ccbca commitfed46bd

File tree

10 files changed

+693
-6
lines changed

10 files changed

+693
-6
lines changed

‎CLAUDE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#Coder Desktop Development Guide
2+
3+
##Build & Test Commands
4+
- Build Xcode project:`make`
5+
- Format Swift files:`make fmt`
6+
- Lint Swift files:`make lint`
7+
- Run all tests:`make test`
8+
- Run specific test class:`xcodebuild test -project "Coder Desktop/Coder Desktop.xcodeproj" -scheme "Coder Desktop" -only-testing:"Coder DesktopTests/AgentsTests"`
9+
- Run specific test method:`xcodebuild test -project "Coder Desktop/Coder Desktop.xcodeproj" -scheme "Coder Desktop" -only-testing:"Coder DesktopTests/AgentsTests/agentsWhenVPNOff"`
10+
- Generate Swift from proto:`make proto`
11+
- Watch for project changes:`make watch-gen`
12+
13+
##Code Style Guidelines
14+
- Use Swift 6.0 for development
15+
- Follow SwiftFormat and SwiftLint rules
16+
- Use Swift's Testing framework for tests (`@Test`,`#expect` directives)
17+
- Group files logically (Views, Models, Extensions)
18+
- Use environment objects for dependency injection
19+
- Prefer async/await over completion handlers
20+
- Use clear, descriptive naming for functions and variables
21+
- Implement proper error handling with Swift's throwing functions
22+
- Tests should use descriptive names reflecting what they're testing

‎Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4949
name:.NEVPNStatusDidChange,
5050
object:nil
5151
)
52+
// Subscribe to reconfiguration requests
53+
NotificationCenter.default.addObserver(
54+
self,
55+
selector: #selector(networkExtensionNeedsReconfiguration(_:)),
56+
name:.networkExtensionNeedsReconfiguration,
57+
object:nil
58+
)
5259
Task{
5360
// If there's no NE config, but the user is logged in, such as
5461
// from a previous install, then we need to reconfigure.
@@ -82,9 +89,27 @@ extension AppDelegate {
8289
vpn.vpnDidUpdate(connection)
8390
menuBar?.vpnDidUpdate(connection)
8491
}
92+
93+
@objcprivatefunc networkExtensionNeedsReconfiguration(_:Notification){
94+
// Check if we have a session
95+
if state.hasSession{
96+
// Reconfigure the network extension with full credentials
97+
state.reconfigure()
98+
}else{
99+
// No valid session, the user likely needs to log in again
100+
// Show the login window
101+
NSApp.sendAction(#selector(NSApp.showLoginWindow), to:nil, from:nil)
102+
}
103+
}
85104
}
86105

87106
@MainActor
88107
func appActivate(){
89108
NSApp.activate()
90109
}
110+
111+
extensionNSApplication{
112+
@objcfunc showLoginWindow(){
113+
NSApp.sendAction(#selector(NSWindowController.showWindow(_:)), to:nil, from:Windows.login.rawValue)
114+
}
115+
}

‎Coder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ final class PreviewVPN: Coder_Desktop.VPNService {
2626
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"example",
2727
wsID:UUID()),
2828
], workspaces:[:])
29+
@PublishedvarsysExtnState:SystemExtensionState=.installed
30+
@PublishedvarneState:NetworkExtensionState=.enabled
2931
letshouldFail:Bool
3032
letlongError="This is a long error to test the UI with long error messages"
3133

32-
init(shouldFail:Bool=false){
34+
init(shouldFail:Bool=false, extensionInstalled:Bool=true, networkExtensionEnabled:Bool=true){
3335
self.shouldFail= shouldFail
36+
sysExtnState= extensionInstalled?.installed:.uninstalled
37+
neState= networkExtensionEnabled?.enabled:.disabled
3438
}
3539

3640
varstartTask:Task<Void,Never>?
@@ -78,4 +82,69 @@ final class PreviewVPN: Coder_Desktop.VPNService {
7882
func configureTunnelProviderProtocol(proto _: NETunnelProviderProtocol?){
7983
state=.connecting
8084
}
85+
86+
func uninstall()async-> Bool{
87+
// Simulate uninstallation with a delay
88+
do{
89+
tryawaitTask.sleep(for:.seconds(2))
90+
}catch{
91+
returnfalse
92+
}
93+
94+
if !shouldFail{
95+
sysExtnState=.uninstalled
96+
returntrue
97+
}
98+
returnfalse
99+
}
100+
101+
func installExtension()async{
102+
// Simulate installation with a delay
103+
do{
104+
tryawaitTask.sleep(for:.seconds(2))
105+
sysExtnState=if !shouldFail{
106+
.installed
107+
}else{
108+
.failed("Failed to install extension")
109+
}
110+
}catch{
111+
sysExtnState=.failed("Installation was interrupted")
112+
}
113+
}
114+
115+
func disableExtension()async-> Bool{
116+
// Simulate disabling with a delay
117+
do{
118+
tryawaitTask.sleep(for:.seconds(1))
119+
}catch{
120+
returnfalse
121+
}
122+
123+
if !shouldFail{
124+
neState=.disabled
125+
state=.disabled
126+
returntrue
127+
}else{
128+
neState=.failed("Failed to disable network extension")
129+
returnfalse
130+
}
131+
}
132+
133+
func enableExtension()async-> Bool{
134+
// Simulate enabling with a delay
135+
do{
136+
tryawaitTask.sleep(for:.seconds(1))
137+
}catch{
138+
returnfalse
139+
}
140+
141+
if !shouldFail{
142+
neState=.enabled
143+
state=.disabled // Just disabled, not connected yet
144+
returntrue
145+
}else{
146+
neState=.failed("Failed to enable network extension")
147+
returnfalse
148+
}
149+
}
81150
}

‎Coder Desktop/Coder Desktop/State.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class AppState: ObservableObject {
88
letappId=Bundle.main.bundleIdentifier!
99

1010
// Stored in UserDefaults
11-
@Publishedprivate(set)varhasSession:Bool{
11+
@PublishedvarhasSession:Bool{
1212
didSet{
1313
guard persistentelse{return}
1414
UserDefaults.standard.set(hasSession, forKey:Keys.hasSession)

‎Coder Desktop/Coder Desktop/SystemExtension.swift

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,121 @@ extension CoderVPNService: SystemExtensionAsyncRecorder {
8181
OSSystemExtensionManager.shared.submitRequest(request)
8282
logger.info("submitted SystemExtension request with bundleID:\(bundleID)")
8383
}
84+
85+
func deregisterSystemExtension()async->Bool{
86+
logger.info("Starting network extension deregistration...")
87+
88+
// Extension bundle identifier - must match what's used in the app
89+
letextensionBundleIdentifier="com.coder.Coder-Desktop.VPN"
90+
91+
returnawaitwithCheckedContinuation{ continuationin
92+
// Create a task to handle the deregistration with timeout
93+
lettimeoutTask=Task{
94+
// Set a timeout for the operation
95+
lettimeoutInterval:TimeInterval=30.0 // 30 seconds
96+
97+
// Use a custom holder for the delegate to keep it alive
98+
// and store the result from the callback
99+
finalclassDelegateHolder{
100+
vardelegate:DeregistrationDelegate?
101+
varresult:Bool?
102+
}
103+
104+
letholder=DelegateHolder()
105+
106+
// Create the delegate with a completion handler
107+
letdelegate=DeregistrationDelegate(completionHandler:{ resultin
108+
holder.result= result
109+
})
110+
holder.delegate= delegate
111+
112+
// Create and submit the deactivation request
113+
letrequest=OSSystemExtensionRequest.deactivationRequest(
114+
forExtensionWithIdentifier: extensionBundleIdentifier,
115+
queue:.main
116+
)
117+
request.delegate= delegate
118+
119+
// Submit the request on the main thread
120+
awaitMainActor.run{
121+
OSSystemExtensionManager.shared.submitRequest(request)
122+
}
123+
124+
// Set up timeout using a separate task
125+
lettimeoutDate=Date().addingTimeInterval(timeoutInterval)
126+
127+
// Wait for completion or timeout
128+
while holder.result==nil,Date()< timeoutDate{
129+
// Sleep a bit before checking again (100ms)
130+
try?awaitTask.sleep(nanoseconds:100_000_000)
131+
132+
// Check for cancellation
133+
ifTask.isCancelled{
134+
break
135+
}
136+
}
137+
138+
// Handle the result
139+
iflet result= holder.result{
140+
logger.info("System extension deregistration completed with result:\(result)")
141+
return result
142+
}else{
143+
logger.error("System extension deregistration timed out after\(timeoutInterval) seconds")
144+
returnfalse
145+
}
146+
}
147+
148+
// Use Task.detached to handle potential continuation issues
149+
Task.detached{
150+
letresult=await timeoutTask.value
151+
continuation.resume(returning: result)
152+
}
153+
}
154+
}
155+
156+
// A dedicated delegate class for system extension deregistration
157+
privateclassDeregistrationDelegate:NSObject,OSSystemExtensionRequestDelegate{
158+
privatevarlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"vpn-deregistrar")
159+
privatevarcompletionHandler:(Bool)->Void
160+
161+
init(completionHandler:@escaping(Bool)->Void){
162+
self.completionHandler= completionHandler
163+
super.init()
164+
}
165+
166+
func request(_:OSSystemExtensionRequest, didFinishWithResult result:OSSystemExtensionRequest.Result){
167+
switch result{
168+
case.completed:
169+
logger.info("System extension was successfully deregistered")
170+
completionHandler(true)
171+
case.willCompleteAfterReboot:
172+
logger.info("System extension will be deregistered after reboot")
173+
completionHandler(true)
174+
@unknowndefault:
175+
logger.error("System extension deregistration completed with unknown result")
176+
completionHandler(false)
177+
}
178+
}
179+
180+
func request(_:OSSystemExtensionRequest, didFailWithError error:Error){
181+
logger.error("System extension deregistration failed:\(error.localizedDescription)")
182+
completionHandler(false)
183+
}
184+
185+
func requestNeedsUserApproval(_:OSSystemExtensionRequest){
186+
logger.info("System extension deregistration needs user approval")
187+
// We don't complete here, as we'll get another callback when approval is granted or denied
188+
}
189+
190+
func request(
191+
_:OSSystemExtensionRequest,
192+
actionForReplacingExtension _:OSSystemExtensionProperties,
193+
withExtension _:OSSystemExtensionProperties
194+
)->OSSystemExtensionRequest.ReplacementAction{
195+
logger.info("System extension replacement request")
196+
return.replace
197+
}
198+
}
84199
}
85200

86201
/// A delegate for the OSSystemExtensionRequest that maps the callbacks to async calls on the

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp