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

Commitff033e1

Browse files
feat: add file sync daemon error handling to the UI (#122)
If file sync is working, but a session has errored, an icon will be displayed on the main menu.e.g. for:<img width="512" alt="image" src="https://github.com/user-attachments/assets/aac73c99-e318-44d3-9091-3f5d99239037" />This icon & tooltip are displayed:<img width="256" alt="image" src="https://github.com/user-attachments/assets/f4b7ba15-dca2-4819-aaa8-07e74e4e238d" />If file sync is not working altogether, due to the daemon crashing, the same icon will be displayed with a different tooltip on hover:<img width="254" alt="image" src="https://github.com/user-attachments/assets/efc87c1d-acac-4353-a3c9-c04908762d28" />Once the config menu is opened, an alert is displayed, and the daemon log file is opened.<img width="1354" alt="image" src="https://github.com/user-attachments/assets/98b44f6e-4584-4ad3-a237-1557ec5edab1" />From there, the Daemon can be restarted, or the alert can be dismissed without restarting. The latter provides users an out if the daemon were to crash on launch repeatedly.
1 parent6463de0 commitff033e1

File tree

5 files changed

+189
-89
lines changed

5 files changed

+189
-89
lines changed

‎Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import VPNLib
22

33
@MainActor
44
finalclassPreviewFileSync:FileSyncDaemon{
5+
varlogFile:URL=.init(filePath:"~/log.txt")!
6+
57
varsessionState:[VPNLib.FileSyncSession]=[]
68

79
varstate:DaemonState=.running
@@ -10,7 +12,7 @@ final class PreviewFileSync: FileSyncDaemon {
1012

1113
func refreshSessions()async{}
1214

13-
funcstart()asyncthrows(DaemonError){
15+
functryStart()async{
1416
state=.running
1517
}
1618

‎Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift

Lines changed: 123 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
1111

1212
@Stateprivatevarloading:Bool=false
1313
@StateprivatevardeleteError:DaemonError?
14+
@StateprivatevarisVisible:Bool=false
15+
@StateprivatevardontRetry:Bool=false
1416

1517
varbody:someView{
1618
Group{
@@ -36,87 +38,140 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
3638
.frame(minWidth:400, minHeight:200)
3739
.padding(.bottom,25)
3840
.overlay(alignment:.bottom){
39-
VStack(alignment:.leading, spacing:0){
40-
Divider()
41-
HStack(spacing:0){
42-
Button{
43-
addingNewSession=true
44-
} label:{
45-
Image(systemName:"plus")
46-
.frame(width:24, height:24)
47-
}.disabled(vpn.menuState.agents.isEmpty)
41+
tableFooter
42+
}
43+
// Only the table & footer should be disabled if the daemon has crashed
44+
// otherwise the alert buttons will be disabled too
45+
}.disabled(fileSync.state.isFailed)
46+
.sheet(isPresented: $addingNewSession){
47+
FileSyncSessionModal<VPN,FS>()
48+
.frame(width:700)
49+
}.sheet(item: $editingSession){ sessionin
50+
FileSyncSessionModal<VPN,FS>(existingSession: session)
51+
.frame(width:700)
52+
}.alert("Error", isPresented:Binding(
53+
get:{ deleteError!=nil},
54+
set:{ isPresentedin
55+
if !isPresented{
56+
deleteError=nil
57+
}
58+
}
59+
)){} message:{
60+
Text(deleteError?.description??"An unknown error occurred.")
61+
}.alert("Error", isPresented:Binding(
62+
// We only show the alert if the file config window is open
63+
// Users will see the alert symbol on the menu bar to prompt them to
64+
// open it. The requirement on `!loading` prevents the alert from
65+
// re-opening immediately.
66+
get:{ !loading && isVisible && fileSync.state.isFailed},
67+
set:{ isPresentedin
68+
if !isPresented{
69+
if dontRetry{
70+
dontRetry=false
71+
return
72+
}
73+
loading=true
74+
Task{
75+
await fileSync.tryStart()
76+
loading=false
77+
}
78+
}
79+
}
80+
)){
81+
Button("Retry"){}
82+
// This gives the user an out if the daemon is crashing on launch,
83+
// they can cancel the alert, and it will reappear if they re-open the
84+
// file sync window.
85+
Button("Cancel", role:.cancel){
86+
dontRetry=true
87+
}
88+
} message:{
89+
Text("""
90+
File sync daemon failed. The daemon log file at\n\(fileSync.logFile.path)\nhas been opened.
91+
""").onAppear{
92+
// Open the log file in the default editor
93+
NSWorkspace.shared.open(fileSync.logFile)
94+
}
95+
}.task{
96+
// When the Window is visible, poll for session updates every
97+
// two seconds.
98+
while !Task.isCancelled{
99+
if !fileSync.state.isFailed{
100+
await fileSync.refreshSessions()
101+
}
102+
try?awaitTask.sleep(for:.seconds(2))
103+
}
104+
}.onAppear{
105+
isVisible=true
106+
}.onDisappear{
107+
isVisible=false
108+
// If the failure alert is dismissed without restarting the daemon,
109+
// (by clicking cancel) this makes it clear that the daemon
110+
// is still in a failed state.
111+
}.navigationTitle("Coder File Sync\(fileSync.state.isFailed?"- Failed":"")")
112+
.disabled(loading)
113+
}
114+
115+
vartableFooter:someView{
116+
VStack(alignment:.leading, spacing:0){
117+
Divider()
118+
HStack(spacing:0){
119+
Button{
120+
addingNewSession=true
121+
} label:{
122+
Image(systemName:"plus")
123+
.frame(width:24, height:24)
124+
}.disabled(vpn.menuState.agents.isEmpty)
125+
Divider()
126+
Button{
127+
Task{
128+
loading=true
129+
defer{ loading=false}
130+
dothrows(DaemonError){
131+
// TODO: Support selecting & deleting multiple sessions at once
132+
tryawait fileSync.deleteSessions(ids:[selection!])
133+
if fileSync.sessionState.isEmpty{
134+
// Last session was deleted, stop the daemon
135+
await fileSync.stop()
136+
}
137+
} catch{
138+
deleteError= error
139+
}
140+
selection=nil
141+
}
142+
} label:{
143+
Image(systemName:"minus").frame(width:24, height:24)
144+
}.disabled(selection==nil)
145+
iflet selection{
146+
iflet selectedSession= fileSync.sessionState.first(where:{ $0.id== selection}){
48147
Divider()
49148
Button{
50149
Task{
150+
// TODO: Support pausing & resuming multiple sessions at once
51151
loading=true
52152
defer{ loading=false}
53-
dothrows(DaemonError){
54-
// TODO: Support selecting & deleting multiple sessions at once
55-
tryawait fileSync.deleteSessions(ids:[selection!])
56-
if fileSync.sessionState.isEmpty{
57-
// Last session was deleted, stop the daemon
58-
await fileSync.stop()
59-
}
60-
} catch{
61-
deleteError= error
153+
switch selectedSession.status{
154+
case.paused:
155+
tryawait fileSync.resumeSessions(ids:[selectedSession.id])
156+
default:
157+
tryawait fileSync.pauseSessions(ids:[selectedSession.id])
62158
}
63-
selection=nil
64159
}
65160
} label:{
66-
Image(systemName:"minus").frame(width:24, height:24)
67-
}.disabled(selection==nil)
68-
iflet selection{
69-
iflet selectedSession= fileSync.sessionState.first(where:{ $0.id== selection}){
70-
Divider()
71-
Button{
72-
Task{
73-
// TODO: Support pausing & resuming multiple sessions at once
74-
loading=true
75-
defer{ loading=false}
76-
switch selectedSession.status{
77-
case.paused:
78-
tryawait fileSync.resumeSessions(ids:[selectedSession.id])
79-
default:
80-
tryawait fileSync.pauseSessions(ids:[selectedSession.id])
81-
}
82-
}
83-
} label:{
84-
switch selectedSession.status{
85-
case.paused:
86-
Image(systemName:"play").frame(width:24, height:24)
87-
default:
88-
Image(systemName:"pause").frame(width:24, height:24)
89-
}
90-
}
161+
switch selectedSession.status{
162+
case.paused:
163+
Image(systemName:"play").frame(width:24, height:24)
164+
default:
165+
Image(systemName:"pause").frame(width:24, height:24)
91166
}
92167
}
93168
}
94-
.buttonStyle(.borderless)
95169
}
96-
.background(.primary.opacity(0.04))
97-
.fixedSize(horizontal:false, vertical:true)
98-
}
99-
}.sheet(isPresented: $addingNewSession){
100-
FileSyncSessionModal<VPN,FS>()
101-
.frame(width:700)
102-
}.sheet(item: $editingSession){ sessionin
103-
FileSyncSessionModal<VPN,FS>(existingSession: session)
104-
.frame(width:700)
105-
}.alert("Error", isPresented:Binding(
106-
get:{ deleteError!=nil},
107-
set:{ isPresentedin
108-
if !isPresented{
109-
deleteError=nil
110-
}
111-
}
112-
)){} message:{
113-
Text(deleteError?.description??"An unknown error occurred.")
114-
}.task{
115-
while !Task.isCancelled{
116-
await fileSync.refreshSessions()
117-
try?awaitTask.sleep(for:.seconds(2))
118170
}
119-
}.disabled(loading)
171+
.buttonStyle(.borderless)
172+
}
173+
.background(.primary.opacity(0.04))
174+
.fixedSize(horizontal:false, vertical:true)
120175
}
121176
}
122177

‎Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenu.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,12 @@ struct VPNMenu<VPN: VPNService, FS: FileSyncDaemon>: View {
6868
} label:{
6969
ButtonRowView{
7070
HStack{
71-
// TODO: A future PR will provide users a way to recover from a daemon failure without
72-
// needing to restart the app
73-
if case.failed= fileSync.state,sessionsHaveError(fileSync.sessionState){
71+
if fileSync.state.isFailed ||sessionsHaveError(fileSync.sessionState){
7472
Image(systemName:"exclamationmark.arrow.trianglehead.2.clockwise.rotate.90")
75-
.frame(width:12, height:12).help("One or more sync sessions have errors")
73+
.frame(width:12, height:12)
74+
.help(fileSync.state.isFailed?
75+
"The file sync daemon encountered an error":
76+
"One or more file sync sessions have errors")
7677
}
7778
Text("File sync")
7879
}

‎Coder-Desktop/Coder-DesktopTests/Util.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class MockVPNService: VPNService, ObservableObject {
2929

3030
@MainActor
3131
classMockFileSyncDaemon:FileSyncDaemon{
32+
varlogFile:URL=.init(filePath:"~/log.txt")
33+
3234
varsessionState:[VPNLib.FileSyncSession]=[]
3335

3436
func refreshSessions()async{}
@@ -37,9 +39,7 @@ class MockFileSyncDaemon: FileSyncDaemon {
3739

3840
varstate:VPNLib.DaemonState=.running
3941

40-
func start()asyncthrows(VPNLib.DaemonError){
41-
return
42-
}
42+
func tryStart()async{}
4343

4444
func stop()async{}
4545

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp