From cb25fe59d78ee7718fe628317cf2cd84c143fcef Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 24 Mar 2025 15:36:09 +1100 Subject: [PATCH 1/6] chore: create & delete sync sessions over gRPC --- .../VPNLib/FileSync/FileSyncDaemon.swift | 47 +---------- .../VPNLib/FileSync/FileSyncManagement.swift | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+), 46 deletions(-) create mode 100644 Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index eafd4dc7..1dd6b958 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -162,7 +162,7 @@ public class MutagenDaemon: FileSyncDaemon { // Already connected return } - group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + group = MultiThreadedEventLoopGroup(numberOfThreads: 2) do { channel = try GRPCChannelPool.with( target: .unixDomainSocket(mutagenDaemonSocket.path), @@ -252,51 +252,6 @@ public class MutagenDaemon: FileSyncDaemon { logger.info("\(line, privacy: .public)") } } - - public func refreshSessions() async { - guard case .running = state else { return } - // TODO: Implement - } - - public func createSession( - localPath _: String, - agentHost _: String, - remotePath _: String - ) async throws(DaemonError) { - if case .stopped = state { - do throws(DaemonError) { - try await start() - } catch { - state = .failed(error) - throw error - } - } - // TODO: Add session - } - - public func deleteSessions(ids _: [String]) async throws(DaemonError) { - // TODO: Delete session - await stopIfNoSessions() - } - - private func stopIfNoSessions() async { - let sessions: Synchronization_ListResponse - do { - sessions = try await client!.sync.list(Synchronization_ListRequest.with { req in - req.selection = .with { selection in - selection.all = true - } - }) - } catch { - state = .failed(.daemonStartFailure(error)) - return - } - // If there's no configured sessions, the daemon doesn't need to be running - if sessions.sessionStates.isEmpty { - logger.info("No sync sessions found") - await stop() - } - } } struct DaemonClient { diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift new file mode 100644 index 00000000..e654866a --- /dev/null +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -0,0 +1,80 @@ +public extension MutagenDaemon { + func refreshSessions() async { + guard case .running = state else { return } + let sessions: Synchronization_ListResponse + do { + sessions = try await client!.sync.list(Synchronization_ListRequest.with { req in + req.selection = .with { selection in + selection.all = true + } + }) + } catch { + state = .failed(.grpcFailure(error)) + return + } + sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) } + if sessionState.isEmpty { + logger.info("No sync sessions found") + await stop() + } + } + + func createSession( + localPath: String, + agentHost: String, + remotePath: String + ) async throws(DaemonError) { + if case .stopped = state { + do throws(DaemonError) { + try await start() + } catch { + state = .failed(error) + throw error + } + } + let (stream, promptID) = try await host() + defer { stream.cancel() } + let req = Synchronization_CreateRequest.with { req in + req.prompter = promptID + req.specification = .with { spec in + spec.alpha = .with { alpha in + alpha.protocol = .local + alpha.path = localPath + } + spec.beta = .with { beta in + beta.protocol = .ssh + beta.host = agentHost + beta.path = remotePath + } + // TODO: Ingest a config from somewhere + spec.configuration = Synchronization_Configuration() + spec.configurationAlpha = Synchronization_Configuration() + spec.configurationBeta = Synchronization_Configuration() + } + } + do { + _ = try await client!.sync.create(req) + } catch { + throw .grpcFailure(error) + } + await refreshSessions() + } + + func deleteSessions(ids: [String]) async throws(DaemonError) { + // Terminating sessions does not require prompting + let (stream, promptID) = try await host(allowPrompts: false) + defer { stream.cancel() } + guard case .running = state else { return } + do { + _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in + req.prompter = promptID + req.selection = .with { selection in + selection.specifications = ids + } + }) + } catch { + throw .grpcFailure(error) + } + await refreshSessions() + } +} From 127807063eae536e4c9c6ba253b64836ee6f35af Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 24 Mar 2025 23:32:25 +1100 Subject: [PATCH 2/6] pausing & unpausing --- .../Preview Content/PreviewFileSync.swift | 4 +++ .../Views/FileSync/FileSyncConfig.swift | 10 +++++- .../VPNLib/FileSync/FileSyncDaemon.swift | 2 ++ .../VPNLib/FileSync/FileSyncManagement.swift | 34 +++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift index 8db30e3c..082c144f 100644 --- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift +++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift @@ -21,4 +21,8 @@ final class PreviewFileSync: FileSyncDaemon { func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {} func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} + + func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} + + func resumeSessions(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 dc83c17a..1abc8e8e 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift @@ -65,7 +65,15 @@ struct FileSyncConfig: View { if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) { Divider() Button { - // TODO: Pause & Unpause + Task { + // TODO: Support pausing & resuming multiple selections + switch selectedSession.status { + case .paused: + try await fileSync.resumeSessions(ids: [selectedSession.id]) + default: + try await fileSync.pauseSessions(ids: [selectedSession.id]) + } + } } label: { switch selectedSession.status { case .paused: diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 1dd6b958..641f4e59 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -15,6 +15,8 @@ public protocol FileSyncDaemon: ObservableObject { func refreshSessions() async func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError) func deleteSessions(ids: [String]) async throws(DaemonError) + func pauseSessions(ids: [String]) async throws(DaemonError) + func resumeSessions(ids: [String]) async throws(DaemonError) } @MainActor diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index e654866a..1bc4c0c0 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -77,4 +77,38 @@ public extension MutagenDaemon { } await refreshSessions() } + + func pauseSessions(ids: [String]) async throws(DaemonError) { + let (stream, promptID) = try await host() + defer { stream.cancel() } + guard case .running = state else { return } + do { + _ = try await client!.sync.pause(Synchronization_PauseRequest.with { req in + req.prompter = promptID + req.selection = .with { selection in + selection.specifications = ids + } + }) + } catch { + throw .grpcFailure(error) + } + await refreshSessions() + } + + func resumeSessions(ids: [String]) async throws(DaemonError) { + let (stream, promptID) = try await host() + defer { stream.cancel() } + guard case .running = state else { return } + do { + _ = try await client!.sync.resume(Synchronization_ResumeRequest.with { req in + req.prompter = promptID + req.selection = .with { selection in + selection.specifications = ids + } + }) + } catch { + throw .grpcFailure(error) + } + await refreshSessions() + } } From dfd1bc8eaf3f2207895fdaa1f5d58a67e61a5229 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 24 Mar 2025 23:34:51 +1100 Subject: [PATCH 3/6] fixup --- .../Views/FileSync/FileSyncConfig.swift | 10 ++++++++-- .../Views/FileSync/FileSyncSessionModal.swift | 1 - Coder-Desktop/Coder-DesktopTests/Util.swift | 4 ++++ .../VPNLib/FileSync/FileSyncDaemon.swift | 4 ++++ .../VPNLib/FileSync/FileSyncManagement.swift | 15 ++++++++------- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift index 1abc8e8e..5a7257b0 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift @@ -51,11 +51,15 @@ struct FileSyncConfig: View { 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 } - await fileSync.refreshSessions() selection = nil } } label: { @@ -66,7 +70,9 @@ struct FileSyncConfig: View { Divider() Button { Task { - // TODO: Support pausing & resuming multiple selections + // 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]) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index c0c7a35b..2539e9db 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -83,7 +83,6 @@ struct FileSyncSessionModal: View { defer { loading = false } do throws(DaemonError) { if let existingSession { - // TODO: Support selecting & deleting multiple sessions at once try await fileSync.deleteSessions(ids: [existingSession.id]) } try await fileSync.createSession( diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift index e38fe330..cad7eaca 100644 --- a/Coder-Desktop/Coder-DesktopTests/Util.swift +++ b/Coder-Desktop/Coder-DesktopTests/Util.swift @@ -48,6 +48,10 @@ class MockFileSyncDaemon: FileSyncDaemon { } func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {} + + func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} + + func resumeSessions(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 641f4e59..4fa76115 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -77,6 +77,10 @@ public class MutagenDaemon: FileSyncDaemon { return } await refreshSessions() + if sessionState.isEmpty { + logger.info("No sync sessions found on startup, stopping daemon") + await stop() + } } } diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index 1bc4c0c0..8c4eb0c6 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -13,10 +13,6 @@ public extension MutagenDaemon { return } sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) } - if sessionState.isEmpty { - logger.info("No sync sessions found") - await stop() - } } func createSession( @@ -61,7 +57,8 @@ public extension MutagenDaemon { } func deleteSessions(ids: [String]) async throws(DaemonError) { - // Terminating sessions does not require prompting + // Terminating sessions does not require prompting, according to the + // Mutagen CLI let (stream, promptID) = try await host(allowPrompts: false) defer { stream.cancel() } guard case .running = state else { return } @@ -79,7 +76,9 @@ public extension MutagenDaemon { } func pauseSessions(ids: [String]) async throws(DaemonError) { - let (stream, promptID) = try await host() + // Pausing sessions does not require prompting, according to the + // Mutagen CLI + let (stream, promptID) = try await host(allowPrompts: false) defer { stream.cancel() } guard case .running = state else { return } do { @@ -96,7 +95,9 @@ public extension MutagenDaemon { } func resumeSessions(ids: [String]) async throws(DaemonError) { - let (stream, promptID) = try await host() + // Resuming sessions does not require prompting, according to the + // Mutagen CLI + let (stream, promptID) = try await host(allowPrompts: false) defer { stream.cancel() } guard case .running = state else { return } do { From 1b6444429a3f299bbf6270d3c60610bceb129eb7 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 25 Mar 2025 16:53:21 +1100 Subject: [PATCH 4/6] set generous timeouts on session requests --- Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift | 3 +++ Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 4fa76115..11b42af9 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -43,6 +43,9 @@ public class MutagenDaemon: FileSyncDaemon { private let mutagenDataDirectory: URL private let mutagenDaemonSocket: URL + // Managing sync sessions can take a while, especially with prompting + let sessionMgmtReqTimeout: TimeAmount = .seconds(5) + // Non-nil when the daemon is running var client: DaemonClient? private var group: MultiThreadedEventLoopGroup? diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index 8c4eb0c6..1be95a60 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -1,3 +1,5 @@ +import NIOCore + public extension MutagenDaemon { func refreshSessions() async { guard case .running = state else { return } @@ -49,7 +51,7 @@ public extension MutagenDaemon { } } do { - _ = try await client!.sync.create(req) + _ = try await client!.sync.create(req, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) } catch { throw .grpcFailure(error) } @@ -68,7 +70,7 @@ public extension MutagenDaemon { req.selection = .with { selection in selection.specifications = ids } - }) + }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) } catch { throw .grpcFailure(error) } @@ -87,7 +89,7 @@ public extension MutagenDaemon { req.selection = .with { selection in selection.specifications = ids } - }) + }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) } catch { throw .grpcFailure(error) } @@ -106,7 +108,7 @@ public extension MutagenDaemon { req.selection = .with { selection in selection.specifications = ids } - }) + }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) } catch { throw .grpcFailure(error) } From 91a5b36307f4327ddf55ea95213f90fef0bfbcf7 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Mar 2025 23:26:22 +1100 Subject: [PATCH 5/6] very important fix --- .../Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index 2539e9db..d3981723 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -68,7 +68,7 @@ struct FileSyncSessionModal: View { }.disabled(loading) .alert("Error", isPresented: Binding( get: { createError != nil }, - set: { if $0 { createError = nil } } + set: { if !$0 { createError = nil } } )) {} message: { Text(createError?.description ?? "An unknown error occurred.") } From 58f9775efba9e7b229034fa99f7a827d52c6c3eb Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 26 Mar 2025 23:43:26 +1100 Subject: [PATCH 6/6] bump timeouts --- Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift | 4 ++-- Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 11b42af9..2adce4b2 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -43,8 +43,8 @@ public class MutagenDaemon: FileSyncDaemon { private let mutagenDataDirectory: URL private let mutagenDaemonSocket: URL - // Managing sync sessions can take a while, especially with prompting - let sessionMgmtReqTimeout: TimeAmount = .seconds(5) + // Managing sync sessions could take a while, especially with prompting + let sessionMgmtReqTimeout: TimeAmount = .seconds(15) // Non-nil when the daemon is running var client: DaemonClient? diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index 1be95a60..c826fa76 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -51,7 +51,10 @@ public extension MutagenDaemon { } } do { - _ = try await client!.sync.create(req, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout))) + // The first creation will need to transfer the agent binary + // TODO: Because this is pretty long, we should show progress updates + // using the prompter messages + _ = try await client!.sync.create(req, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout * 4))) } catch { throw .grpcFailure(error) } 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