Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9a2f1cf

Browse files
committedMar 10, 2025
chore: manage mutagen daemon lifecycle
1 parent b7ccbca commit 9a2f1cf

16 files changed

+693
-7
lines changed
 

‎.swiftlint.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# TODO: Remove this once the grpc-swift-protobuf generator adds a lint disable comment
2+
excluded:
3+
- "**/*.pb.swift"
4+
- "**/*.grpc.swift"

‎Coder Desktop/.swiftformat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
--selfrequired log,info,error,debug,critical,fault
2-
--exclude **.pb.swift
2+
--exclude **.pb.swift,**.grpc.swift
33
--condassignment always

‎Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3030
private var menuBar: MenuBarController?
3131
let vpn: CoderVPNService
3232
let state: AppState
33+
let fileSyncDaemon: MutagenDaemon
3334

3435
override init() {
3536
vpn = CoderVPNService()
3637
state = AppState(onChange: vpn.configureTunnelProviderProtocol)
38+
fileSyncDaemon = MutagenDaemon()
3739
}
3840

3941
func applicationDidFinishLaunching(_: Notification) {
@@ -56,14 +58,25 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5658
state.reconfigure()
5759
}
5860
}
61+
// TODO: Start the daemon only once a file sync is configured
62+
Task {
63+
try? await fileSyncDaemon.start()
64+
}
5965
}
6066

6167
// This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
6268
// or return `.terminateNow`
6369
func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
64-
if !state.stopVPNOnQuit { return .terminateNow }
6570
Task {
66-
await vpn.stop()
71+
let vpnStop = Task {
72+
if !state.stopVPNOnQuit {
73+
await vpn.stop()
74+
}
75+
}
76+
let fileSyncStop = Task {
77+
try? await fileSyncDaemon.stop()
78+
}
79+
_ = await (vpnStop.value, fileSyncStop.value)
6780
NSApp.reply(toApplicationShouldTerminate: true)
6881
}
6982
return .terminateLater
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import Foundation
2+
import GRPC
3+
import NIO
4+
import os
5+
6+
@MainActor
7+
protocol FileSyncDaemon: ObservableObject {
8+
var state: DaemonState { get }
9+
func start() async throws
10+
func stop() async throws
11+
}
12+
13+
@MainActor
14+
class MutagenDaemon: FileSyncDaemon {
15+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen")
16+
17+
@Published var state: DaemonState = .stopped
18+
19+
private var mutagenProcess: Process?
20+
private var mutagenPipe: Pipe?
21+
private let mutagenPath: URL
22+
private let mutagenDataDirectory: URL
23+
private let mutagenDaemonSocket: URL
24+
25+
private var group: MultiThreadedEventLoopGroup?
26+
private var channel: GRPCChannel?
27+
private var client: Daemon_DaemonAsyncClient?
28+
29+
init() {
30+
#if arch(arm64)
31+
mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-arm64", withExtension: nil)!
32+
#elseif arch(x86_64)
33+
mutagenPath = Bundle.main.url(forResource: "mutagen-darwin-amd64", withExtension: nil)!
34+
#else
35+
fatalError("unknown architecture")
36+
#endif
37+
mutagenDataDirectory = FileManager.default.urls(
38+
for: .applicationSupportDirectory,
39+
in: .userDomainMask
40+
).first!.appending(path: "Coder Desktop").appending(path: "Mutagen")
41+
mutagenDaemonSocket = mutagenDataDirectory.appending(path: "daemon").appending(path: "daemon.sock")
42+
// It shouldn't be fatal if the app was built without Mutagen embedded,
43+
// but file sync will be unavailable.
44+
if !FileManager.default.fileExists(atPath: mutagenPath.path) {
45+
logger.warning("Mutagen not embedded in app, file sync will be unavailable")
46+
state = .unavailable
47+
}
48+
}
49+
50+
func start() async throws {
51+
if case .unavailable = state { return }
52+
53+
// Stop an orphaned daemon, if there is one
54+
try? await connect()
55+
try? await stop()
56+
57+
(mutagenProcess, mutagenPipe) = createMutagenProcess()
58+
do {
59+
try mutagenProcess?.run()
60+
} catch {
61+
state = .failed("Failed to start file sync daemon: \(error)")
62+
throw MutagenDaemonError.daemonStartFailure(error)
63+
}
64+
65+
try await connect()
66+
67+
state = .running
68+
}
69+
70+
private func connect() async throws {
71+
guard client == nil else {
72+
// Already connected
73+
return
74+
}
75+
group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
76+
do {
77+
channel = try GRPCChannelPool.with(
78+
target: .unixDomainSocket(mutagenDaemonSocket.path),
79+
transportSecurity: .plaintext,
80+
eventLoopGroup: group!
81+
)
82+
client = Daemon_DaemonAsyncClient(channel: channel!)
83+
logger.info("Successfully connected to mutagen daemon via gRPC")
84+
} catch {
85+
logger.error("Failed to connect to gRPC: \(error)")
86+
try await cleanupGRPC()
87+
throw MutagenDaemonError.connectionFailure(error)
88+
}
89+
}
90+
91+
private func cleanupGRPC() async throws {
92+
try? await channel?.close().get()
93+
try? await group?.shutdownGracefully()
94+
95+
client = nil
96+
channel = nil
97+
group = nil
98+
}
99+
100+
func stop() async throws {
101+
if case .unavailable = state { return }
102+
state = .stopped
103+
guard FileManager.default.fileExists(atPath: mutagenDaemonSocket.path) else {
104+
return
105+
}
106+
107+
// "We don't check the response or error, because the daemon
108+
// may terminate before it has a chance to send the response."
109+
_ = try? await client?.terminate(
110+
Daemon_TerminateRequest(),
111+
callOptions: .init(timeLimit: .timeout(.milliseconds(500)))
112+
)
113+
114+
// Clean up gRPC connection
115+
try? await cleanupGRPC()
116+
117+
// Ensure the process is terminated
118+
mutagenProcess?.terminate()
119+
logger.info("Daemon stopped and gRPC connection closed")
120+
}
121+
122+
private func createMutagenProcess() -> (Process, Pipe) {
123+
let outputPipe = Pipe()
124+
outputPipe.fileHandleForReading.readabilityHandler = logOutput
125+
let process = Process()
126+
process.executableURL = mutagenPath
127+
process.arguments = ["daemon", "run"]
128+
process.environment = [
129+
"MUTAGEN_DATA_DIRECTORY": mutagenDataDirectory.path,
130+
]
131+
process.standardOutput = outputPipe
132+
process.standardError = outputPipe
133+
process.terminationHandler = terminationHandler
134+
return (process, outputPipe)
135+
}
136+
137+
private nonisolated func terminationHandler(process _: Process) {
138+
Task { @MainActor in
139+
self.mutagenPipe?.fileHandleForReading.readabilityHandler = nil
140+
mutagenProcess = nil
141+
142+
try? await cleanupGRPC()
143+
144+
switch self.state {
145+
case .stopped:
146+
logger.info("mutagen daemon stopped")
147+
return
148+
default:
149+
logger.error("mutagen daemon exited unexpectedly")
150+
self.state = .failed("File sync daemon terminated unexpectedly")
151+
}
152+
}
153+
}
154+
155+
private nonisolated func logOutput(pipe: FileHandle) {
156+
if let line = String(data: pipe.availableData, encoding: .utf8), line != "" {
157+
logger.info("\(line)")
158+
}
159+
}
160+
}
161+
162+
enum DaemonState {
163+
case running
164+
case stopped
165+
case failed(String)
166+
case unavailable
167+
}
168+
169+
enum MutagenDaemonError: Error {
170+
case daemonStartFailure(Error)
171+
case connectionFailure(Error)
172+
}
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
//
2+
// DO NOT EDIT.
3+
// swift-format-ignore-file
4+
//
5+
// Generated by the protocol buffer compiler.
6+
// Source: Coder Desktop/Coder Desktop/FileSync/daemon.proto
7+
//
8+
import GRPC
9+
import NIO
10+
import NIOConcurrencyHelpers
11+
import SwiftProtobuf
12+
13+
14+
/// Usage: instantiate `Daemon_DaemonClient`, then call methods of this protocol to make API calls.
15+
internal protocol Daemon_DaemonClientProtocol: GRPCClient {
16+
var serviceName: String { get }
17+
var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }
18+
19+
func terminate(
20+
_ request: Daemon_TerminateRequest,
21+
callOptions: CallOptions?
22+
) -> UnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse>
23+
}
24+
25+
extension Daemon_DaemonClientProtocol {
26+
internal var serviceName: String {
27+
return "daemon.Daemon"
28+
}
29+
30+
/// Unary call to Terminate
31+
///
32+
/// - Parameters:
33+
/// - request: Request to send to Terminate.
34+
/// - callOptions: Call options.
35+
/// - Returns: A `UnaryCall` with futures for the metadata, status and response.
36+
internal func terminate(
37+
_ request: Daemon_TerminateRequest,
38+
callOptions: CallOptions? = nil
39+
) -> UnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse> {
40+
return self.makeUnaryCall(
41+
path: Daemon_DaemonClientMetadata.Methods.terminate.path,
42+
request: request,
43+
callOptions: callOptions ?? self.defaultCallOptions,
44+
interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
45+
)
46+
}
47+
}
48+
49+
@available(*, deprecated)
50+
extension Daemon_DaemonClient: @unchecked Sendable {}
51+
52+
@available(*, deprecated, renamed: "Daemon_DaemonNIOClient")
53+
internal final class Daemon_DaemonClient: Daemon_DaemonClientProtocol {
54+
private let lock = Lock()
55+
private var _defaultCallOptions: CallOptions
56+
private var _interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?
57+
internal let channel: GRPCChannel
58+
internal var defaultCallOptions: CallOptions {
59+
get { self.lock.withLock { return self._defaultCallOptions } }
60+
set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }
61+
}
62+
internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? {
63+
get { self.lock.withLock { return self._interceptors } }
64+
set { self.lock.withLockVoid { self._interceptors = newValue } }
65+
}
66+
67+
/// Creates a client for the daemon.Daemon service.
68+
///
69+
/// - Parameters:
70+
/// - channel: `GRPCChannel` to the service host.
71+
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
72+
/// - interceptors: A factory providing interceptors for each RPC.
73+
internal init(
74+
channel: GRPCChannel,
75+
defaultCallOptions: CallOptions = CallOptions(),
76+
interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
77+
) {
78+
self.channel = channel
79+
self._defaultCallOptions = defaultCallOptions
80+
self._interceptors = interceptors
81+
}
82+
}
83+
84+
internal struct Daemon_DaemonNIOClient: Daemon_DaemonClientProtocol {
85+
internal var channel: GRPCChannel
86+
internal var defaultCallOptions: CallOptions
87+
internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?
88+
89+
/// Creates a client for the daemon.Daemon service.
90+
///
91+
/// - Parameters:
92+
/// - channel: `GRPCChannel` to the service host.
93+
/// - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
94+
/// - interceptors: A factory providing interceptors for each RPC.
95+
internal init(
96+
channel: GRPCChannel,
97+
defaultCallOptions: CallOptions = CallOptions(),
98+
interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
99+
) {
100+
self.channel = channel
101+
self.defaultCallOptions = defaultCallOptions
102+
self.interceptors = interceptors
103+
}
104+
}
105+
106+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
107+
internal protocol Daemon_DaemonAsyncClientProtocol: GRPCClient {
108+
static var serviceDescriptor: GRPCServiceDescriptor { get }
109+
var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? { get }
110+
111+
func makeTerminateCall(
112+
_ request: Daemon_TerminateRequest,
113+
callOptions: CallOptions?
114+
) -> GRPCAsyncUnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse>
115+
}
116+
117+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
118+
extension Daemon_DaemonAsyncClientProtocol {
119+
internal static var serviceDescriptor: GRPCServiceDescriptor {
120+
return Daemon_DaemonClientMetadata.serviceDescriptor
121+
}
122+
123+
internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? {
124+
return nil
125+
}
126+
127+
internal func makeTerminateCall(
128+
_ request: Daemon_TerminateRequest,
129+
callOptions: CallOptions? = nil
130+
) -> GRPCAsyncUnaryCall<Daemon_TerminateRequest, Daemon_TerminateResponse> {
131+
return self.makeAsyncUnaryCall(
132+
path: Daemon_DaemonClientMetadata.Methods.terminate.path,
133+
request: request,
134+
callOptions: callOptions ?? self.defaultCallOptions,
135+
interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
136+
)
137+
}
138+
}
139+
140+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
141+
extension Daemon_DaemonAsyncClientProtocol {
142+
internal func terminate(
143+
_ request: Daemon_TerminateRequest,
144+
callOptions: CallOptions? = nil
145+
) async throws -> Daemon_TerminateResponse {
146+
return try await self.performAsyncUnaryCall(
147+
path: Daemon_DaemonClientMetadata.Methods.terminate.path,
148+
request: request,
149+
callOptions: callOptions ?? self.defaultCallOptions,
150+
interceptors: self.interceptors?.makeTerminateInterceptors() ?? []
151+
)
152+
}
153+
}
154+
155+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
156+
internal struct Daemon_DaemonAsyncClient: Daemon_DaemonAsyncClientProtocol {
157+
internal var channel: GRPCChannel
158+
internal var defaultCallOptions: CallOptions
159+
internal var interceptors: Daemon_DaemonClientInterceptorFactoryProtocol?
160+
161+
internal init(
162+
channel: GRPCChannel,
163+
defaultCallOptions: CallOptions = CallOptions(),
164+
interceptors: Daemon_DaemonClientInterceptorFactoryProtocol? = nil
165+
) {
166+
self.channel = channel
167+
self.defaultCallOptions = defaultCallOptions
168+
self.interceptors = interceptors
169+
}
170+
}
171+
172+
internal protocol Daemon_DaemonClientInterceptorFactoryProtocol: Sendable {
173+
174+
/// - Returns: Interceptors to use when invoking 'terminate'.
175+
func makeTerminateInterceptors() -> [ClientInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
176+
}
177+
178+
internal enum Daemon_DaemonClientMetadata {
179+
internal static let serviceDescriptor = GRPCServiceDescriptor(
180+
name: "Daemon",
181+
fullName: "daemon.Daemon",
182+
methods: [
183+
Daemon_DaemonClientMetadata.Methods.terminate,
184+
]
185+
)
186+
187+
internal enum Methods {
188+
internal static let terminate = GRPCMethodDescriptor(
189+
name: "Terminate",
190+
path: "/daemon.Daemon/Terminate",
191+
type: GRPCCallType.unary
192+
)
193+
}
194+
}
195+
196+
/// To build a server, implement a class that conforms to this protocol.
197+
internal protocol Daemon_DaemonProvider: CallHandlerProvider {
198+
var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }
199+
200+
func terminate(request: Daemon_TerminateRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Daemon_TerminateResponse>
201+
}
202+
203+
extension Daemon_DaemonProvider {
204+
internal var serviceName: Substring {
205+
return Daemon_DaemonServerMetadata.serviceDescriptor.fullName[...]
206+
}
207+
208+
/// Determines, calls and returns the appropriate request handler, depending on the request's method.
209+
/// Returns nil for methods not handled by this service.
210+
internal func handle(
211+
method name: Substring,
212+
context: CallHandlerContext
213+
) -> GRPCServerHandlerProtocol? {
214+
switch name {
215+
case "Terminate":
216+
return UnaryServerHandler(
217+
context: context,
218+
requestDeserializer: ProtobufDeserializer<Daemon_TerminateRequest>(),
219+
responseSerializer: ProtobufSerializer<Daemon_TerminateResponse>(),
220+
interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
221+
userFunction: self.terminate(request:context:)
222+
)
223+
224+
default:
225+
return nil
226+
}
227+
}
228+
}
229+
230+
/// To implement a server, implement an object which conforms to this protocol.
231+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
232+
internal protocol Daemon_DaemonAsyncProvider: CallHandlerProvider, Sendable {
233+
static var serviceDescriptor: GRPCServiceDescriptor { get }
234+
var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? { get }
235+
236+
func terminate(
237+
request: Daemon_TerminateRequest,
238+
context: GRPCAsyncServerCallContext
239+
) async throws -> Daemon_TerminateResponse
240+
}
241+
242+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
243+
extension Daemon_DaemonAsyncProvider {
244+
internal static var serviceDescriptor: GRPCServiceDescriptor {
245+
return Daemon_DaemonServerMetadata.serviceDescriptor
246+
}
247+
248+
internal var serviceName: Substring {
249+
return Daemon_DaemonServerMetadata.serviceDescriptor.fullName[...]
250+
}
251+
252+
internal var interceptors: Daemon_DaemonServerInterceptorFactoryProtocol? {
253+
return nil
254+
}
255+
256+
internal func handle(
257+
method name: Substring,
258+
context: CallHandlerContext
259+
) -> GRPCServerHandlerProtocol? {
260+
switch name {
261+
case "Terminate":
262+
return GRPCAsyncServerHandler(
263+
context: context,
264+
requestDeserializer: ProtobufDeserializer<Daemon_TerminateRequest>(),
265+
responseSerializer: ProtobufSerializer<Daemon_TerminateResponse>(),
266+
interceptors: self.interceptors?.makeTerminateInterceptors() ?? [],
267+
wrapping: { try await self.terminate(request: $0, context: $1) }
268+
)
269+
270+
default:
271+
return nil
272+
}
273+
}
274+
}
275+
276+
internal protocol Daemon_DaemonServerInterceptorFactoryProtocol: Sendable {
277+
278+
/// - Returns: Interceptors to use when handling 'terminate'.
279+
/// Defaults to calling `self.makeInterceptors()`.
280+
func makeTerminateInterceptors() -> [ServerInterceptor<Daemon_TerminateRequest, Daemon_TerminateResponse>]
281+
}
282+
283+
internal enum Daemon_DaemonServerMetadata {
284+
internal static let serviceDescriptor = GRPCServiceDescriptor(
285+
name: "Daemon",
286+
fullName: "daemon.Daemon",
287+
methods: [
288+
Daemon_DaemonServerMetadata.Methods.terminate,
289+
]
290+
)
291+
292+
internal enum Methods {
293+
internal static let terminate = GRPCMethodDescriptor(
294+
name: "Terminate",
295+
path: "/daemon.Daemon/Terminate",
296+
type: GRPCCallType.unary
297+
)
298+
}
299+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// DO NOT EDIT.
2+
// swift-format-ignore-file
3+
// swiftlint:disable all
4+
//
5+
// Generated by the Swift generator plugin for the protocol buffer compiler.
6+
// Source: Coder Desktop/Coder Desktop/FileSync/daemon.proto
7+
//
8+
// For information on using the generated types, please see the documentation:
9+
// https://github.com/apple/swift-protobuf/
10+
11+
import SwiftProtobuf
12+
13+
// If the compiler emits an error on this type, it is because this file
14+
// was generated by a version of the `protoc` Swift plug-in that is
15+
// incompatible with the version of SwiftProtobuf to which you are linking.
16+
// Please ensure that you are building against the same version of the API
17+
// that was used to generate this file.
18+
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
19+
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
20+
typealias Version = _2
21+
}
22+
23+
struct Daemon_TerminateRequest: Sendable {
24+
// SwiftProtobuf.Message conformance is added in an extension below. See the
25+
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
26+
// methods supported on all messages.
27+
28+
var unknownFields = SwiftProtobuf.UnknownStorage()
29+
30+
init() {}
31+
}
32+
33+
struct Daemon_TerminateResponse: Sendable {
34+
// SwiftProtobuf.Message conformance is added in an extension below. See the
35+
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
36+
// methods supported on all messages.
37+
38+
var unknownFields = SwiftProtobuf.UnknownStorage()
39+
40+
init() {}
41+
}
42+
43+
// MARK: - Code below here is support for the SwiftProtobuf runtime.
44+
45+
fileprivate let _protobuf_package = "daemon"
46+
47+
extension Daemon_TerminateRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
48+
static let protoMessageName: String = _protobuf_package + ".TerminateRequest"
49+
static let _protobuf_nameMap = SwiftProtobuf._NameMap()
50+
51+
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
52+
// Load everything into unknown fields
53+
while try decoder.nextFieldNumber() != nil {}
54+
}
55+
56+
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
57+
try unknownFields.traverse(visitor: &visitor)
58+
}
59+
60+
static func ==(lhs: Daemon_TerminateRequest, rhs: Daemon_TerminateRequest) -> Bool {
61+
if lhs.unknownFields != rhs.unknownFields {return false}
62+
return true
63+
}
64+
}
65+
66+
extension Daemon_TerminateResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
67+
static let protoMessageName: String = _protobuf_package + ".TerminateResponse"
68+
static let _protobuf_nameMap = SwiftProtobuf._NameMap()
69+
70+
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
71+
// Load everything into unknown fields
72+
while try decoder.nextFieldNumber() != nil {}
73+
}
74+
75+
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
76+
try unknownFields.traverse(visitor: &visitor)
77+
}
78+
79+
static func ==(lhs: Daemon_TerminateResponse, rhs: Daemon_TerminateResponse) -> Bool {
80+
if lhs.unknownFields != rhs.unknownFields {return false}
81+
return true
82+
}
83+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
syntax = "proto3";
2+
3+
package daemon;
4+
5+
message TerminateRequest{}
6+
7+
message TerminateResponse{}
8+
9+
service Daemon {
10+
rpc Terminate(TerminateRequest) returns (TerminateResponse) {}
11+
}

‎Coder Desktop/Coder Desktop/State.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import KeychainAccess
44
import NetworkExtension
55
import SwiftUI
66

7+
@MainActor
78
class AppState: ObservableObject {
89
let appId = Bundle.main.bundleIdentifier!
910

‎Coder Desktop/project.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,19 @@ packages:
105105
LaunchAtLogin:
106106
url: https://github.com/sindresorhus/LaunchAtLogin-modern
107107
from: 1.1.0
108+
GRPC:
109+
url: https://github.com/grpc/grpc-swift
110+
# v2 does not support macOS 14.0
111+
exactVersion: 1.24.2
108112

109113
targets:
110114
Coder Desktop:
111115
type: application
112116
platform: macOS
113117
sources:
114118
- path: Coder Desktop
119+
- path: Resources
120+
buildPhase: resources
115121
entitlements:
116122
path: Coder Desktop/Coder_Desktop.entitlements
117123
properties:
@@ -155,6 +161,10 @@ targets:
155161
- package: FluidMenuBarExtra
156162
- package: KeychainAccess
157163
- package: LaunchAtLogin
164+
- package: GRPC
165+
- package: SwiftProtobuf
166+
- package: SwiftProtobuf
167+
product: SwiftProtobufPluginLibrary
158168
scheme:
159169
testPlans:
160170
- path: Coder Desktop.xctestplan

‎Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ APP_SIGNING_KEYCHAIN := $(if $(wildcard $(KEYCHAIN_FILE)),$(shell realpath $(KEY
3333
.PHONY: setup
3434
setup: \
3535
$(XCPROJECT) \
36-
$(PROJECT)/VPNLib/vpn.pb.swift
36+
proto
3737

3838
$(XCPROJECT): $(PROJECT)/project.yml
3939
cd $(PROJECT); \
@@ -48,6 +48,12 @@ $(XCPROJECT): $(PROJECT)/project.yml
4848
$(PROJECT)/VPNLib/vpn.pb.swift: $(PROJECT)/VPNLib/vpn.proto
4949
protoc --swift_opt=Visibility=public --swift_out=. 'Coder Desktop/VPNLib/vpn.proto'
5050

51+
$(PROJECT)/Coder\ Desktop/FileSync/daemon.pb.swift: $(PROJECT)/Coder\ Desktop/FileSync/daemon.proto
52+
protoc \
53+
--swift_out=.\
54+
--grpc-swift_out=. \
55+
'Coder Desktop/Coder Desktop/FileSync/daemon.proto'
56+
5157
$(KEYCHAIN_FILE):
5258
security create-keychain -p "" "$(APP_SIGNING_KEYCHAIN)"
5359
security set-keychain-settings -lut 21600 "$(APP_SIGNING_KEYCHAIN)"
@@ -130,7 +136,7 @@ clean/build:
130136
rm -rf build/ release/ $$out
131137

132138
.PHONY: proto
133-
proto: $(PROJECT)/VPNLib/vpn.pb.swift ## Generate Swift files from protobufs
139+
proto: $(PROJECT)/VPNLib/vpn.pb.swift $(PROJECT)/Coder\ Desktop/FileSync/daemon.pb.swift ## Generate Swift files from protobufs
134140

135141
.PHONY: help
136142
help: ## Show this help

‎flake.lock

Lines changed: 85 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎flake.nix

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
inputs = {
55
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
66
flake-utils.url = "github:numtide/flake-utils";
7+
grpc-swift.url = "github:i10416/grpc-swift-flake";
78
};
89

910
outputs =
1011
{
1112
self,
1213
nixpkgs,
1314
flake-utils,
15+
grpc-swift,
1416
}:
1517
flake-utils.lib.eachSystem
1618
(with flake-utils.lib.system; [
@@ -40,7 +42,8 @@
4042
git
4143
gnumake
4244
protobuf_28
43-
protoc-gen-swift
45+
grpc-swift.packages.${system}.protoc-gen-grpc-swift
46+
grpc-swift.packages.${system}.protoc-gen-swift
4447
swiftformat
4548
swiftlint
4649
xcbeautify

0 commit comments

Comments
 (0)
Please sign in to comment.