Skip to content

Commit ce6cdcc

Browse files
committed
feat: support restarting file sync sessions
1 parent be70ade commit ce6cdcc

File tree

5 files changed

+79
-28
lines changed

5 files changed

+79
-28
lines changed

Diff for: Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift

+2
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ final class PreviewFileSync: FileSyncDaemon {
2727
func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
2828

2929
func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
30+
31+
func resetSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
3032
}

Diff for: Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift

+54-25
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
1010
@State private var editingSession: FileSyncSession?
1111

1212
@State private var loading: Bool = false
13-
@State private var deleteError: DaemonError?
13+
@State private var actionError: DaemonError?
1414
@State private var isVisible: Bool = false
1515
@State private var dontRetry: Bool = false
1616

@@ -50,14 +50,14 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
5050
FileSyncSessionModal<VPN, FS>(existingSession: session)
5151
.frame(width: 700)
5252
}.alert("Error", isPresented: Binding(
53-
get: { deleteError != nil },
53+
get: { actionError != nil },
5454
set: { isPresented in
5555
if !isPresented {
56-
deleteError = nil
56+
actionError = nil
5757
}
5858
}
5959
)) {} message: {
60-
Text(deleteError?.description ?? "An unknown error occurred.")
60+
Text(actionError?.description ?? "An unknown error occurred.")
6161
}.alert("Error", isPresented: Binding(
6262
// We only show the alert if the file config window is open
6363
// Users will see the alert symbol on the menu bar to prompt them to
@@ -118,7 +118,7 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
118118
addingNewSession = true
119119
} label: {
120120
Image(systemName: "plus")
121-
.frame(width: 24, height: 24)
121+
.frame(width: 24, height: 24).help("Create")
122122
}.disabled(vpn.menuState.agents.isEmpty)
123123
Divider()
124124
Button {
@@ -133,43 +133,72 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
133133
await fileSync.stop()
134134
}
135135
} catch {
136-
deleteError = error
136+
actionError = error
137137
}
138138
selection = nil
139139
}
140140
} label: {
141-
Image(systemName: "minus").frame(width: 24, height: 24)
141+
Image(systemName: "minus").frame(width: 24, height: 24).help("Delete")
142142
}.disabled(selection == nil)
143-
if let selection {
144-
if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
145-
Divider()
146-
Button {
147-
Task {
148-
// TODO: Support pausing & resuming multiple sessions at once
149-
loading = true
150-
defer { loading = false }
143+
sessionControls
144+
}
145+
.buttonStyle(.borderless)
146+
}
147+
.background(.primary.opacity(0.04))
148+
.fixedSize(horizontal: false, vertical: true)
149+
}
150+
151+
var sessionControls: some View {
152+
Group {
153+
if let selection {
154+
if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
155+
Divider()
156+
Button {
157+
Task {
158+
// TODO: Support pausing & resuming multiple sessions at once
159+
loading = true
160+
defer { loading = false }
161+
do throws(DaemonError) {
151162
switch selectedSession.status {
152-
case .paused:
163+
case .paused, .error(.haltedOnRootEmptied),
164+
.error(.haltedOnRootDeletion),
165+
.error(.haltedOnRootTypeChange):
153166
try await fileSync.resumeSessions(ids: [selectedSession.id])
154167
default:
155168
try await fileSync.pauseSessions(ids: [selectedSession.id])
156169
}
170+
} catch {
171+
actionError = error
157172
}
158-
} label: {
159-
switch selectedSession.status {
160-
case .paused:
161-
Image(systemName: "play").frame(width: 24, height: 24)
162-
default:
163-
Image(systemName: "pause").frame(width: 24, height: 24)
173+
}
174+
} label: {
175+
switch selectedSession.status {
176+
case .paused, .error(.haltedOnRootEmptied),
177+
.error(.haltedOnRootDeletion),
178+
.error(.haltedOnRootTypeChange):
179+
Image(systemName: "play").frame(width: 24, height: 24).help("Pause")
180+
default:
181+
Image(systemName: "pause").frame(width: 24, height: 24).help("Resume")
182+
}
183+
}
184+
Divider()
185+
Button {
186+
Task {
187+
// TODO: Support restarting multiple sessions at once
188+
loading = true
189+
defer { loading = false }
190+
do throws(DaemonError) {
191+
try await fileSync.resetSessions(ids: [selectedSession.id])
192+
} catch {
193+
actionError = error
164194
}
165195
}
196+
} label: {
197+
Image(systemName: "arrow.clockwise").frame(width: 24, height: 24).help("Reset")
166198
}
167199
}
168200
}
169-
.buttonStyle(.borderless)
170201
}
171-
.background(.primary.opacity(0.04))
172-
.fixedSize(horizontal: false, vertical: true)
173202
}
174203
}
175204

Diff for: Coder-Desktop/Coder-DesktopTests/Util.swift

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class MockFileSyncDaemon: FileSyncDaemon {
5252
func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
5353

5454
func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
55+
56+
func resetSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
5557
}
5658

5759
extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}

Diff for: Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public protocol FileSyncDaemon: ObservableObject {
1818
func deleteSessions(ids: [String]) async throws(DaemonError)
1919
func pauseSessions(ids: [String]) async throws(DaemonError)
2020
func resumeSessions(ids: [String]) async throws(DaemonError)
21+
func resetSessions(ids: [String]) async throws(DaemonError)
2122
}
2223

2324
@MainActor

Diff for: Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift

+20-3
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,8 @@ public extension MutagenDaemon {
100100
}
101101

102102
func resumeSessions(ids: [String]) async throws(DaemonError) {
103-
// Resuming sessions does not require prompting, according to the
104-
// Mutagen CLI
105-
let (stream, promptID) = try await host(allowPrompts: false)
103+
// Resuming sessions does use prompting, as it may start a new SSH connection
104+
let (stream, promptID) = try await host(allowPrompts: true)
106105
defer { stream.cancel() }
107106
guard case .running = state else { return }
108107
do {
@@ -117,4 +116,22 @@ public extension MutagenDaemon {
117116
}
118117
await refreshSessions()
119118
}
119+
120+
func resetSessions(ids: [String]) async throws(DaemonError) {
121+
// Resetting a session involves pausing & resuming, so it does use prompting
122+
let (stream, promptID) = try await host(allowPrompts: true)
123+
defer { stream.cancel() }
124+
guard case .running = state else { return }
125+
do {
126+
_ = try await client!.sync.reset(Synchronization_ResetRequest.with { req in
127+
req.prompter = promptID
128+
req.selection = .with { selection in
129+
selection.specifications = ids
130+
}
131+
}, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
132+
} catch {
133+
throw .grpcFailure(error)
134+
}
135+
await refreshSessions()
136+
}
120137
}

0 commit comments

Comments
 (0)