diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 00633744..eafd4dc7 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -19,7 +19,7 @@ public protocol FileSyncDaemon: ObservableObject { @MainActor public class MutagenDaemon: FileSyncDaemon { - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen") + let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen") @Published public var state: DaemonState = .stopped { didSet { @@ -42,9 +42,9 @@ public class MutagenDaemon: FileSyncDaemon { private let mutagenDaemonSocket: URL // Non-nil when the daemon is running + var client: DaemonClient? private var group: MultiThreadedEventLoopGroup? private var channel: GRPCChannel? - private var client: DaemonClient? // Protect start & stop transitions against re-entrancy private let transition = AsyncSemaphore(value: 1) @@ -171,7 +171,8 @@ public class MutagenDaemon: FileSyncDaemon { ) client = DaemonClient( mgmt: Daemon_DaemonAsyncClient(channel: channel!), - sync: Synchronization_SynchronizationAsyncClient(channel: channel!) + sync: Synchronization_SynchronizationAsyncClient(channel: channel!), + prompt: Prompting_PromptingAsyncClient(channel: channel!) ) logger.info( "Successfully connected to mutagen daemon, socket: \(self.mutagenDaemonSocket.path, privacy: .public)" @@ -301,6 +302,7 @@ public class MutagenDaemon: FileSyncDaemon { struct DaemonClient { let mgmt: Daemon_DaemonAsyncClient let sync: Synchronization_SynchronizationAsyncClient + let prompt: Prompting_PromptingAsyncClient } public enum DaemonState { @@ -342,6 +344,8 @@ public enum DaemonError: Error { case connectionFailure(Error) case terminatedUnexpectedly case grpcFailure(Error) + case invalidGrpcResponse(String) + case unexpectedStreamClosure public var description: String { switch self { @@ -355,6 +359,10 @@ public enum DaemonError: Error { "The daemon must be started first" case let .grpcFailure(error): "Failed to communicate with daemon: \(error)" + case let .invalidGrpcResponse(response): + "Invalid gRPC response: \(response)" + case .unexpectedStreamClosure: + "Unexpected stream closure" } } diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift new file mode 100644 index 00000000..d5a49b42 --- /dev/null +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift @@ -0,0 +1,53 @@ +import GRPC + +extension MutagenDaemon { + typealias PromptStream = GRPCAsyncBidirectionalStreamingCall + + func host(allowPrompts: Bool = true) async throws(DaemonError) -> (PromptStream, identifier: String) { + let stream = client!.prompt.makeHostCall() + + do { + try await stream.requestStream.send(.with { req in req.allowPrompts = allowPrompts }) + } catch { + throw .grpcFailure(error) + } + + // We can't make call `makeAsyncIterator` more than once + // (as a for-loop would do implicitly) + var iter = stream.responseStream.makeAsyncIterator() + + let initResp: Prompting_HostResponse? + do { + initResp = try await iter.next() + } catch { + throw .grpcFailure(error) + } + guard let initResp else { + throw .unexpectedStreamClosure + } + try initResp.ensureValid(first: true, allowPrompts: allowPrompts) + + Task.detached(priority: .background) { + do { + while let msg = try await iter.next() { + try msg.ensureValid(first: false, allowPrompts: allowPrompts) + var reply: Prompting_HostRequest = .init() + if msg.isPrompt { + // Handle SSH key prompts + if msg.message.contains("yes/no/[fingerprint]") { + reply.response = "yes" + } + // Any other messages that require a non-empty response will + // cause the create op to fail, showing an error. This is ok for now. + } + try await stream.requestStream.send(reply) + } + } catch let error as GRPCStatus where error.code == .cancelled { + return + } catch { + self.logger.critical("Prompt stream failed: \(error)") + } + } + return (stream, identifier: initResp.identifier) + } +} diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift index 7afefee1..8a59b238 100644 --- a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift +++ b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift @@ -57,3 +57,26 @@ func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] { func humanReadableBytes(_ bytes: UInt64) -> String { ByteCountFormatter().string(fromByteCount: Int64(bytes)) } + +extension Prompting_HostResponse { + func ensureValid(first: Bool, allowPrompts: Bool) throws(DaemonError) { + if first { + if identifier.isEmpty { + throw .invalidGrpcResponse("empty prompter identifier") + } + if isPrompt { + throw .invalidGrpcResponse("unexpected message type specification") + } + if !message.isEmpty { + throw .invalidGrpcResponse("unexpected message") + } + } else { + if !identifier.isEmpty { + throw .invalidGrpcResponse("unexpected prompter identifier") + } + if isPrompt, !allowPrompts { + throw .invalidGrpcResponse("disallowed prompt message type") + } + } + } +} diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.grpc.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.grpc.swift new file mode 100644 index 00000000..a79eb510 --- /dev/null +++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.grpc.swift @@ -0,0 +1,421 @@ +// +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the protocol buffer compiler. +// Source: service_prompting_prompting.proto +// +import GRPC +import NIO +import NIOConcurrencyHelpers +import SwiftProtobuf + + +/// Prompting allows clients to host and request prompting. +/// +/// Usage: instantiate `Prompting_PromptingClient`, then call methods of this protocol to make API calls. +internal protocol Prompting_PromptingClientProtocol: GRPCClient { + var serviceName: String { get } + var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? { get } + + func host( + callOptions: CallOptions?, + handler: @escaping (Prompting_HostResponse) -> Void + ) -> BidirectionalStreamingCall + + func prompt( + _ request: Prompting_PromptRequest, + callOptions: CallOptions? + ) -> UnaryCall +} + +extension Prompting_PromptingClientProtocol { + internal var serviceName: String { + return "prompting.Prompting" + } + + /// Host allows clients to perform prompt hosting. + /// + /// Callers should use the `send` method on the returned object to send messages + /// to the server. The caller should send an `.end` after the final message has been sent. + /// + /// - Parameters: + /// - callOptions: Call options. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. + internal func host( + callOptions: CallOptions? = nil, + handler: @escaping (Prompting_HostResponse) -> Void + ) -> BidirectionalStreamingCall { + return self.makeBidirectionalStreamingCall( + path: Prompting_PromptingClientMetadata.Methods.host.path, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeHostInterceptors() ?? [], + handler: handler + ) + } + + /// Prompt performs prompting using a specific prompter. + /// + /// - Parameters: + /// - request: Request to send to Prompt. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + internal func prompt( + _ request: Prompting_PromptRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Prompting_PromptingClientMetadata.Methods.prompt.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makePromptInterceptors() ?? [] + ) + } +} + +@available(*, deprecated) +extension Prompting_PromptingClient: @unchecked Sendable {} + +@available(*, deprecated, renamed: "Prompting_PromptingNIOClient") +internal final class Prompting_PromptingClient: Prompting_PromptingClientProtocol { + private let lock = Lock() + private var _defaultCallOptions: CallOptions + private var _interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? + internal let channel: GRPCChannel + internal var defaultCallOptions: CallOptions { + get { self.lock.withLock { return self._defaultCallOptions } } + set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } + } + internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? { + get { self.lock.withLock { return self._interceptors } } + set { self.lock.withLockVoid { self._interceptors = newValue } } + } + + /// Creates a client for the prompting.Prompting service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self._defaultCallOptions = defaultCallOptions + self._interceptors = interceptors + } +} + +internal struct Prompting_PromptingNIOClient: Prompting_PromptingClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? + + /// Creates a client for the prompting.Prompting service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +/// Prompting allows clients to host and request prompting. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Prompting_PromptingAsyncClientProtocol: GRPCClient { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? { get } + + func makeHostCall( + callOptions: CallOptions? + ) -> GRPCAsyncBidirectionalStreamingCall + + func makePromptCall( + _ request: Prompting_PromptRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Prompting_PromptingAsyncClientProtocol { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Prompting_PromptingClientMetadata.serviceDescriptor + } + + internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? { + return nil + } + + internal func makeHostCall( + callOptions: CallOptions? = nil + ) -> GRPCAsyncBidirectionalStreamingCall { + return self.makeAsyncBidirectionalStreamingCall( + path: Prompting_PromptingClientMetadata.Methods.host.path, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeHostInterceptors() ?? [] + ) + } + + internal func makePromptCall( + _ request: Prompting_PromptRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Prompting_PromptingClientMetadata.Methods.prompt.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makePromptInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Prompting_PromptingAsyncClientProtocol { + internal func host( + _ requests: RequestStream, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Prompting_HostRequest { + return self.performAsyncBidirectionalStreamingCall( + path: Prompting_PromptingClientMetadata.Methods.host.path, + requests: requests, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeHostInterceptors() ?? [] + ) + } + + internal func host( + _ requests: RequestStream, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Prompting_HostRequest { + return self.performAsyncBidirectionalStreamingCall( + path: Prompting_PromptingClientMetadata.Methods.host.path, + requests: requests, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeHostInterceptors() ?? [] + ) + } + + internal func prompt( + _ request: Prompting_PromptRequest, + callOptions: CallOptions? = nil + ) async throws -> Prompting_PromptResponse { + return try await self.performAsyncUnaryCall( + path: Prompting_PromptingClientMetadata.Methods.prompt.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makePromptInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal struct Prompting_PromptingAsyncClient: Prompting_PromptingAsyncClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? + + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Prompting_PromptingClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +internal protocol Prompting_PromptingClientInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when invoking 'host'. + func makeHostInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'prompt'. + func makePromptInterceptors() -> [ClientInterceptor] +} + +internal enum Prompting_PromptingClientMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "Prompting", + fullName: "prompting.Prompting", + methods: [ + Prompting_PromptingClientMetadata.Methods.host, + Prompting_PromptingClientMetadata.Methods.prompt, + ] + ) + + internal enum Methods { + internal static let host = GRPCMethodDescriptor( + name: "Host", + path: "/prompting.Prompting/Host", + type: GRPCCallType.bidirectionalStreaming + ) + + internal static let prompt = GRPCMethodDescriptor( + name: "Prompt", + path: "/prompting.Prompting/Prompt", + type: GRPCCallType.unary + ) + } +} + +/// Prompting allows clients to host and request prompting. +/// +/// To build a server, implement a class that conforms to this protocol. +internal protocol Prompting_PromptingProvider: CallHandlerProvider { + var interceptors: Prompting_PromptingServerInterceptorFactoryProtocol? { get } + + /// Host allows clients to perform prompt hosting. + func host(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> + + /// Prompt performs prompting using a specific prompter. + func prompt(request: Prompting_PromptRequest, context: StatusOnlyCallContext) -> EventLoopFuture +} + +extension Prompting_PromptingProvider { + internal var serviceName: Substring { + return Prompting_PromptingServerMetadata.serviceDescriptor.fullName[...] + } + + /// Determines, calls and returns the appropriate request handler, depending on the request's method. + /// Returns nil for methods not handled by this service. + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "Host": + return BidirectionalStreamingServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeHostInterceptors() ?? [], + observerFactory: self.host(context:) + ) + + case "Prompt": + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makePromptInterceptors() ?? [], + userFunction: self.prompt(request:context:) + ) + + default: + return nil + } + } +} + +/// Prompting allows clients to host and request prompting. +/// +/// To implement a server, implement an object which conforms to this protocol. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Prompting_PromptingAsyncProvider: CallHandlerProvider, Sendable { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Prompting_PromptingServerInterceptorFactoryProtocol? { get } + + /// Host allows clients to perform prompt hosting. + func host( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws + + /// Prompt performs prompting using a specific prompter. + func prompt( + request: Prompting_PromptRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Prompting_PromptResponse +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Prompting_PromptingAsyncProvider { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Prompting_PromptingServerMetadata.serviceDescriptor + } + + internal var serviceName: Substring { + return Prompting_PromptingServerMetadata.serviceDescriptor.fullName[...] + } + + internal var interceptors: Prompting_PromptingServerInterceptorFactoryProtocol? { + return nil + } + + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "Host": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeHostInterceptors() ?? [], + wrapping: { try await self.host(requestStream: $0, responseStream: $1, context: $2) } + ) + + case "Prompt": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makePromptInterceptors() ?? [], + wrapping: { try await self.prompt(request: $0, context: $1) } + ) + + default: + return nil + } + } +} + +internal protocol Prompting_PromptingServerInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when handling 'host'. + /// Defaults to calling `self.makeInterceptors()`. + func makeHostInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'prompt'. + /// Defaults to calling `self.makeInterceptors()`. + func makePromptInterceptors() -> [ServerInterceptor] +} + +internal enum Prompting_PromptingServerMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "Prompting", + fullName: "prompting.Prompting", + methods: [ + Prompting_PromptingServerMetadata.Methods.host, + Prompting_PromptingServerMetadata.Methods.prompt, + ] + ) + + internal enum Methods { + internal static let host = GRPCMethodDescriptor( + name: "Host", + path: "/prompting.Prompting/Host", + type: GRPCCallType.bidirectionalStreaming + ) + + internal static let prompt = GRPCMethodDescriptor( + name: "Prompt", + path: "/prompting.Prompting/Prompt", + type: GRPCCallType.unary + ) + } +} diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.pb.swift b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.pb.swift new file mode 100644 index 00000000..74afe922 --- /dev/null +++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.pb.swift @@ -0,0 +1,279 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: service_prompting_prompting.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// +// This file was taken from +// https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/prompting/prompting.proto +// +// MIT License +// +// Copyright (c) 2016-present Docker, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// HostRequest encodes either an initial request to perform prompt hosting or a +/// follow-up response to a message or prompt. +struct Prompting_HostRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// AllowPrompts indicates whether or not the hoster will allow prompts. If + /// not, it will only receive message requests. This field may only be set on + /// the initial request. + var allowPrompts: Bool = false + + /// Response is the prompt response, if any. On the initial request, this + /// must be an empty string. When responding to a prompt, it may be any + /// value. When responding to a message, it must be an empty string. + var response: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// HostResponse encodes either an initial response to perform prompt hosting or +/// a follow-up request for messaging or prompting. +struct Prompting_HostResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Identifier is the prompter identifier. It is only set in the initial + /// response sent after the initial request. + var identifier: String = String() + + /// IsPrompt indicates if the response is requesting a prompt (as opposed to + /// simple message display). + var isPrompt: Bool = false + + /// Message is the message associated with the prompt or message. + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// PromptRequest encodes a request for prompting by a specific prompter. +struct Prompting_PromptRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Prompter is the prompter identifier. + var prompter: String = String() + + /// Prompt is the prompt to present. + var prompt: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// PromptResponse encodes the response from a prompter. +struct Prompting_PromptResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Response is the response returned by the prompter. + var response: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "prompting" + +extension Prompting_HostRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HostRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "allowPrompts"), + 2: .same(proto: "response"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.allowPrompts) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.response) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.allowPrompts != false { + try visitor.visitSingularBoolField(value: self.allowPrompts, fieldNumber: 1) + } + if !self.response.isEmpty { + try visitor.visitSingularStringField(value: self.response, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Prompting_HostRequest, rhs: Prompting_HostRequest) -> Bool { + if lhs.allowPrompts != rhs.allowPrompts {return false} + if lhs.response != rhs.response {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Prompting_HostResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HostResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "identifier"), + 2: .same(proto: "isPrompt"), + 3: .same(proto: "message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.identifier) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.isPrompt) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.identifier.isEmpty { + try visitor.visitSingularStringField(value: self.identifier, fieldNumber: 1) + } + if self.isPrompt != false { + try visitor.visitSingularBoolField(value: self.isPrompt, fieldNumber: 2) + } + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Prompting_HostResponse, rhs: Prompting_HostResponse) -> Bool { + if lhs.identifier != rhs.identifier {return false} + if lhs.isPrompt != rhs.isPrompt {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Prompting_PromptRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PromptRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "prompter"), + 2: .same(proto: "prompt"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.prompter) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.prompt) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.prompter.isEmpty { + try visitor.visitSingularStringField(value: self.prompter, fieldNumber: 1) + } + if !self.prompt.isEmpty { + try visitor.visitSingularStringField(value: self.prompt, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Prompting_PromptRequest, rhs: Prompting_PromptRequest) -> Bool { + if lhs.prompter != rhs.prompter {return false} + if lhs.prompt != rhs.prompt {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Prompting_PromptResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PromptResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "response"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.response) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.response.isEmpty { + try visitor.visitSingularStringField(value: self.response, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Prompting_PromptResponse, rhs: Prompting_PromptResponse) -> Bool { + if lhs.response != rhs.response {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.proto b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.proto new file mode 100644 index 00000000..337a1544 --- /dev/null +++ b/Coder-Desktop/VPNLib/FileSync/MutagenSDK/service_prompting_prompting.proto @@ -0,0 +1,80 @@ +/* + * This file was taken from + * https://github.com/mutagen-io/mutagen/tree/v0.18.1/pkg/service/prompting/prompting.proto + * + * MIT License + * + * Copyright (c) 2016-present Docker, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +syntax = "proto3"; + +package prompting; + +option go_package = "github.com/mutagen-io/mutagen/pkg/service/prompting"; + +// HostRequest encodes either an initial request to perform prompt hosting or a +// follow-up response to a message or prompt. +message HostRequest { + // AllowPrompts indicates whether or not the hoster will allow prompts. If + // not, it will only receive message requests. This field may only be set on + // the initial request. + bool allowPrompts = 1; + // Response is the prompt response, if any. On the initial request, this + // must be an empty string. When responding to a prompt, it may be any + // value. When responding to a message, it must be an empty string. + string response = 2; +} + +// HostResponse encodes either an initial response to perform prompt hosting or +// a follow-up request for messaging or prompting. +message HostResponse { + // Identifier is the prompter identifier. It is only set in the initial + // response sent after the initial request. + string identifier = 1; + // IsPrompt indicates if the response is requesting a prompt (as opposed to + // simple message display). + bool isPrompt = 2; + // Message is the message associated with the prompt or message. + string message = 3; +} + +// PromptRequest encodes a request for prompting by a specific prompter. +message PromptRequest { + // Prompter is the prompter identifier. + string prompter = 1; + // Prompt is the prompt to present. + string prompt = 2; +} + +// PromptResponse encodes the response from a prompter. +message PromptResponse { + // Response is the response returned by the prompter. + string response = 1; +} + +// Prompting allows clients to host and request prompting. +service Prompting { + // Host allows clients to perform prompt hosting. + rpc Host(stream HostRequest) returns (stream HostResponse) {} + // Prompt performs prompting using a specific prompter. + rpc Prompt(PromptRequest) returns (PromptResponse) {} +} diff --git a/scripts/mutagen-proto.sh b/scripts/mutagen-proto.sh index 4fc6cf67..fb01413b 100755 --- a/scripts/mutagen-proto.sh +++ b/scripts/mutagen-proto.sh @@ -4,9 +4,9 @@ # It is very similar to `Update-Proto.ps1` on `coder/coder-desktop-windows`. # It's very unlikely that we'll use this script regularly. # -# Unlike the Go compiler, the Swift compiler does not support multiple files -# with the same name in different directories. -# To handle this, this script flattens the directory structure of the proto +# Unlike the Go compiler, the Swift compiler does not support multiple files +# with the same name in different directories. +# To handle this, this script flattens the directory structure of the proto # files into the filename, i.e. `service/synchronization/synchronization.proto` # becomes `service_synchronization_synchronization.proto`. # It also updates the proto imports to use these paths. @@ -24,7 +24,7 @@ mutagen_tag="$1" repo="mutagen-io/mutagen" proto_prefix="pkg" # Right now, we only care about the synchronization and daemon management gRPC -entry_files=("service/synchronization/synchronization.proto" "service/daemon/daemon.proto") +entry_files=("service/synchronization/synchronization.proto" "service/daemon/daemon.proto" "service/prompting/prompting.proto") out_folder="Coder-Desktop/VPNLib/FileSync/MutagenSDK" @@ -33,7 +33,7 @@ if [ -d "$clone_dir" ]; then echo "Found existing mutagen repo at $clone_dir, checking out $mutagen_tag..." pushd "$clone_dir" > /dev/null git clean -fdx - + current_tag=$(git name-rev --name-only HEAD) if [ "$current_tag" != "tags/$mutagen_tag" ]; then git fetch --all @@ -62,27 +62,27 @@ add_file() { local proto_path="${filepath#"$clone_dir"/"$proto_prefix"/}" local flat_name flat_name=$(echo "$proto_path" | sed 's/\//_/g') - + # Skip if already processed if [[ -n "${file_map[$proto_path]:-}" ]]; then return fi - + echo "Adding $proto_path -> $flat_name" file_map[$proto_path]=$flat_name file_paths+=("$filepath") - + # Process imports while IFS= read -r line; do if [[ $line =~ ^import\ \"(.+)\" ]]; then import_path="${BASH_REMATCH[1]}" - + # Ignore google imports, as they're not vendored if [[ $import_path =~ ^google/ ]]; then echo "Skipping $import_path" continue fi - + import_file_path="$clone_dir/$proto_prefix/$import_path" if [ -f "$import_file_path" ]; then add_file "$import_file_path" @@ -109,24 +109,24 @@ for file_path in "${file_paths[@]}"; do proto_path="${file_path#"$clone_dir"/"$proto_prefix"/}" flat_name="${file_map[$proto_path]}" dst_path="$out_folder/$flat_name" - + cp -f "$file_path" "$dst_path" - + file_header="/*\n * This file was taken from\n * https://github.com/$repo/tree/$mutagen_tag/$proto_prefix/$proto_path\n *\n$license_header\n */\n\n" content=$(cat "$dst_path") echo -e "$file_header$content" > "$dst_path" - + tmp_file=$(mktemp) while IFS= read -r line; do if [[ $line =~ ^import\ \"(.+)\" ]]; then import_path="${BASH_REMATCH[1]}" - + # Retain google imports if [[ $import_path =~ ^google/ ]]; then echo "$line" >> "$tmp_file" continue fi - + # Convert import path to flattened format flat_import=$(echo "$import_path" | sed 's/\//_/g') echo "import \"$flat_import\";" >> "$tmp_file" @@ -135,7 +135,7 @@ for file_path in "${file_paths[@]}"; do fi done < "$dst_path" mv "$tmp_file" "$dst_path" - + echo "Processed $proto_path -> $flat_name" done