diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift index 608b3684..45597166 100644 --- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift +++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift @@ -27,4 +27,6 @@ final class PreviewFileSync: FileSyncDaemon { func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} + + func resetSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} } diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift index ff4fbe1a..6b147add 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift @@ -10,7 +10,7 @@ struct FileSyncConfig: View { @State private var editingSession: FileSyncSession? @State private var loading: Bool = false - @State private var deleteError: DaemonError? + @State private var actionError: DaemonError? @State private var isVisible: Bool = false @State private var dontRetry: Bool = false @@ -50,14 +50,14 @@ struct FileSyncConfig: View { FileSyncSessionModal(existingSession: session) .frame(width: 700) }.alert("Error", isPresented: Binding( - get: { deleteError != nil }, + get: { actionError != nil }, set: { isPresented in if !isPresented { - deleteError = nil + actionError = nil } } )) {} message: { - Text(deleteError?.description ?? "An unknown error occurred.") + Text(actionError?.description ?? "An unknown error occurred.") }.alert("Error", isPresented: Binding( // We only show the alert if the file config window is open // Users will see the alert symbol on the menu bar to prompt them to @@ -89,7 +89,7 @@ struct FileSyncConfig: View { Text(""" File sync daemon failed. The daemon log file at\n\(fileSync.logFile.path)\nhas been opened. """).onAppear { - // Open the log file in the default editor + // Opens the log file in Console NSWorkspace.shared.open(fileSync.logFile) } }.task { @@ -120,58 +120,90 @@ struct FileSyncConfig: View { addingNewSession = true } label: { Image(systemName: "plus") - .frame(width: 24, height: 24) + .frame(width: 24, height: 24).help("Create") }.disabled(vpn.menuState.agents.isEmpty) - Divider() - Button { - Task { - loading = true - defer { loading = false } - do throws(DaemonError) { - // TODO: Support selecting & deleting multiple sessions at once - try await fileSync.deleteSessions(ids: [selection!]) - if fileSync.sessionState.isEmpty { - // Last session was deleted, stop the daemon - await fileSync.stop() - } - } catch { - deleteError = error + sessionControls + } + .buttonStyle(.borderless) + } + .background(.primary.opacity(0.04)) + .fixedSize(horizontal: false, vertical: true) + } + + var sessionControls: some View { + Group { + if let selection { + if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) { + Divider() + Button { Task { await delete(session: selectedSession) } } + label: { + Image(systemName: "minus").frame(width: 24, height: 24).help("Terminate") } - selection = nil - } - } label: { - Image(systemName: "minus").frame(width: 24, height: 24) - }.disabled(selection == nil) - if let selection { - if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) { - Divider() - Button { - Task { - // TODO: Support pausing & resuming multiple sessions at once - loading = true - defer { loading = false } - switch selectedSession.status { - case .paused: - try await fileSync.resumeSessions(ids: [selectedSession.id]) - default: - try await fileSync.pauseSessions(ids: [selectedSession.id]) - } - } - } label: { + Divider() + Button { Task { await pauseResume(session: selectedSession) } } + label: { switch selectedSession.status { - case .paused: - Image(systemName: "play").frame(width: 24, height: 24) + case .paused, .error(.haltedOnRootEmptied), + .error(.haltedOnRootDeletion), + .error(.haltedOnRootTypeChange): + Image(systemName: "play").frame(width: 24, height: 24).help("Pause") default: - Image(systemName: "pause").frame(width: 24, height: 24) + Image(systemName: "pause").frame(width: 24, height: 24).help("Resume") } } - } + Divider() + Button { Task { await reset(session: selectedSession) } } + label: { + Image(systemName: "arrow.clockwise").frame(width: 24, height: 24).help("Reset") + } } } - .buttonStyle(.borderless) } - .background(.primary.opacity(0.04)) - .fixedSize(horizontal: false, vertical: true) + } + + // TODO: Support selecting & deleting multiple sessions at once + func delete(session _: FileSyncSession) async { + loading = true + defer { loading = false } + do throws(DaemonError) { + try await fileSync.deleteSessions(ids: [selection!]) + if fileSync.sessionState.isEmpty { + // Last session was deleted, stop the daemon + await fileSync.stop() + } + } catch { + actionError = error + } + selection = nil + } + + // TODO: Support pausing & resuming multiple sessions at once + func pauseResume(session: FileSyncSession) async { + loading = true + defer { loading = false } + do throws(DaemonError) { + switch session.status { + case .paused, .error(.haltedOnRootEmptied), + .error(.haltedOnRootDeletion), + .error(.haltedOnRootTypeChange): + try await fileSync.resumeSessions(ids: [session.id]) + default: + try await fileSync.pauseSessions(ids: [session.id]) + } + } catch { + actionError = error + } + } + + // TODO: Support restarting multiple sessions at once + func reset(session: FileSyncSession) async { + loading = true + defer { loading = false } + do throws(DaemonError) { + try await fileSync.resetSessions(ids: [session.id]) + } catch { + actionError = error + } } } diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift index bfae5167..4301cbc4 100644 --- a/Coder-Desktop/Coder-DesktopTests/Util.swift +++ b/Coder-Desktop/Coder-DesktopTests/Util.swift @@ -52,6 +52,8 @@ class MockFileSyncDaemon: FileSyncDaemon { func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} func resumeSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} + + func resetSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} } extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {} diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 1bac93cb..9e10f2ac 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -18,6 +18,7 @@ public protocol FileSyncDaemon: ObservableObject { func deleteSessions(ids: [String]) async throws(DaemonError) func pauseSessions(ids: [String]) async throws(DaemonError) func resumeSessions(ids: [String]) async throws(DaemonError) + func resetSessions(ids: [String]) async throws(DaemonError) } @MainActor diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index c826fa76..d1d3f6ca 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -100,9 +100,8 @@ public extension MutagenDaemon { } func resumeSessions(ids: [String]) async throws(DaemonError) { - // Resuming sessions does not require prompting, according to the - // Mutagen CLI - let (stream, promptID) = try await host(allowPrompts: false) + // Resuming sessions does use prompting, as it may start a new SSH connection + let (stream, promptID) = try await host(allowPrompts: true) defer { stream.cancel() } guard case .running = state else { return } do { @@ -117,4 +116,22 @@ public extension MutagenDaemon { } await refreshSessions() } + + func resetSessions(ids: [String]) async throws(DaemonError) { + // Resetting a session involves pausing & resuming, so it does use prompting + let (stream, promptID) = try await host(allowPrompts: true) + defer { stream.cancel() } + guard case .running = state else { return } + do { + _ = try await client!.sync.reset(Synchronization_ResetRequest.with { req in + req.prompter = promptID + req.selection = .with { selection in + selection.specifications = ids + } + }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) + } catch { + throw .grpcFailure(error) + } + await refreshSessions() + } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy