Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit4fb7970

Browse files
chore: conditionally start file sync daemon (#115)
This makes a few improvements to#98:- The mutagen path & data directory can be now be configured on the MutagenDaemon, to support overriding it in tests (coming soon).- A mutagen daemon failure now kills the process, such that can it be restarted (TBC).- Makes start & stop transitions mutually exclusive via a semaphore, to account for actor re-entrancy.- The start operation now waits for the daemon to respond to a version request before completing.- The daemon is always started on launch, but then immediately stopped if it doesn't manage any file sync sessions, as to not run in the background unncessarily.
1 parent2603ace commit4fb7970

File tree

3 files changed

+178
-31
lines changed

3 files changed

+178
-31
lines changed

‎Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3636
overrideinit(){
3737
vpn=CoderVPNService()
3838
state=AppState(onChange: vpn.configureTunnelProviderProtocol)
39-
fileSyncDaemon=MutagenDaemon()
4039
if state.startVPNOnLaunch{
4140
vpn.startWhenReady=true
4241
}
4342
vpn.installSystemExtension()
43+
#if arch(arm64)
44+
letmutagenBinary="mutagen-darwin-arm64"
45+
#elseif arch(x86_64)
46+
letmutagenBinary="mutagen-darwin-amd64"
47+
#endif
48+
fileSyncDaemon=MutagenDaemon(
49+
mutagenPath:Bundle.main.url(forResource: mutagenBinary, withExtension:nil)
50+
)
4451
}
4552

4653
func applicationDidFinishLaunching(_:Notification){
@@ -73,10 +80,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
7380
state.reconfigure()
7481
}
7582
}
76-
// TODO: Start the daemon only once a file sync is configured
77-
Task{
78-
await fileSyncDaemon.start()
79-
}
8083
}
8184

8285
deinit{

‎Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift

Lines changed: 165 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,26 @@ import Foundation
22
import GRPC
33
import NIO
44
import os
5+
import Semaphore
56
import Subprocess
7+
import SwiftUI
68

79
@MainActor
810
publicprotocolFileSyncDaemon:ObservableObject{
911
varstate:DaemonState{get}
10-
func start()async
12+
func start()asyncthrows(DaemonError)
1113
func stop()async
14+
func listSessions()asyncthrows->[FileSyncSession]
15+
func createSession(with:FileSyncSession)asyncthrows
16+
}
17+
18+
publicstructFileSyncSession{
19+
publicletid:String
20+
publicletname:String
21+
publicletlocalPath:URL
22+
publicletworkspace:String
23+
publicletagent:String
24+
publicletremotePath:URL
1225
}
1326

1427
@MainActor
@@ -17,7 +30,14 @@ public class MutagenDaemon: FileSyncDaemon {
1730

1831
@Publishedpublicvarstate:DaemonState=.stopped{
1932
didSet{
20-
logger.info("daemon state changed:\(self.state.description, privacy:.public)")
33+
logger.info("daemon state set:\(self.state.description, privacy:.public)")
34+
if case.failed= state{
35+
Task{
36+
try?awaitcleanupGRPC()
37+
}
38+
mutagenProcess?.kill()
39+
mutagenProcess=nil
40+
}
2141
}
2242
}
2343

@@ -26,46 +46,61 @@ public class MutagenDaemon: FileSyncDaemon {
2646
privateletmutagenDataDirectory:URL
2747
privateletmutagenDaemonSocket:URL
2848

49+
// Non-nil when the daemon is running
2950
privatevargroup:MultiThreadedEventLoopGroup?
3051
privatevarchannel:GRPCChannel?
31-
privatevarclient:Daemon_DaemonAsyncClient?
32-
33-
publicinit(){
34-
#if arch(arm64)
35-
mutagenPath=Bundle.main.url(forResource:"mutagen-darwin-arm64", withExtension:nil)
36-
#elseif arch(x86_64)
37-
mutagenPath=Bundle.main.url(forResource:"mutagen-darwin-amd64", withExtension:nil)
38-
#else
39-
fatalError("unknown architecture")
40-
#endif
41-
mutagenDataDirectory=FileManager.default.urls(
42-
for:.applicationSupportDirectory,
43-
in:.userDomainMask
44-
).first!.appending(path:"Coder Desktop").appending(path:"Mutagen")
52+
privatevarclient:DaemonClient?
53+
54+
// Protect start & stop transitions against re-entrancy
55+
privatelettransition=AsyncSemaphore(value:1)
56+
57+
publicinit(mutagenPath:URL?=nil,
58+
mutagenDataDirectory:URL=FileManager.default.urls(
59+
for:.applicationSupportDirectory,
60+
in:.userDomainMask
61+
).first!.appending(path:"Coder Desktop").appending(path:"Mutagen"))
62+
{
63+
self.mutagenPath= mutagenPath
64+
self.mutagenDataDirectory= mutagenDataDirectory
4565
mutagenDaemonSocket= mutagenDataDirectory.appending(path:"daemon").appending(path:"daemon.sock")
4666
// It shouldn't be fatal if the app was built without Mutagen embedded,
4767
// but file sync will be unavailable.
4868
if mutagenPath==nil{
4969
logger.warning("Mutagen not embedded in app, file sync will be unavailable")
5070
state=.unavailable
71+
return
72+
}
73+
74+
// If there are sync sessions, the daemon should be running
75+
Task{
76+
dothrows(DaemonError){
77+
tryawaitstart()
78+
} catch{
79+
state=.failed(error)
80+
return
81+
}
82+
awaitstopIfNoSessions()
5183
}
5284
}
5385

54-
publicfunc start()async{
86+
publicfunc start()asyncthrows(DaemonError){
5587
if case.unavailable= state{return}
5688

5789
// Stop an orphaned daemon, if there is one
5890
try?awaitconnect()
5991
awaitstop()
6092

93+
await transition.wait()
94+
defer{ transition.signal()}
95+
logger.info("starting mutagen daemon")
96+
6197
mutagenProcess=createMutagenProcess()
6298
// swiftlint:disable:next large_tuple
6399
let(standardOutput, standardError, waitForExit):(Pipe.AsyncBytes,Pipe.AsyncBytes,@Sendable()async->Void)
64100
do{
65101
(standardOutput, standardError, waitForExit)=try mutagenProcess!.run()
66102
}catch{
67-
state=.failed(DaemonError.daemonStartFailure(error))
68-
return
103+
throw.daemonStartFailure(error)
69104
}
70105

71106
Task{
@@ -85,10 +120,11 @@ public class MutagenDaemon: FileSyncDaemon {
85120
do{
86121
tryawaitconnect()
87122
}catch{
88-
state=.failed(DaemonError.daemonStartFailure(error))
89-
return
123+
throw.daemonStartFailure(error)
90124
}
91125

126+
tryawaitwaitForDaemonStart()
127+
92128
state=.running
93129
logger.info(
94130
"""
@@ -98,6 +134,34 @@ public class MutagenDaemon: FileSyncDaemon {
98134
)
99135
}
100136

137+
// The daemon takes a moment to open the socket, and we don't want to hog the main actor
138+
// so poll for it on a background thread
139+
privatefunc waitForDaemonStart(
140+
maxAttempts:Int=5,
141+
attemptInterval:Duration=.milliseconds(100)
142+
)asyncthrows(DaemonError){
143+
do{
144+
tryawaitTask.detached(priority:.background){
145+
forattemptin0... maxAttempts{
146+
do{
147+
_=tryawaitself.client!.mgmt.version(
148+
Daemon_VersionRequest(),
149+
callOptions:.init(timeLimit:.timeout(.milliseconds(500)))
150+
)
151+
return
152+
}catch{
153+
if attempt== maxAttempts{
154+
throw error
155+
}
156+
try?awaitTask.sleep(for: attemptInterval)
157+
}
158+
}
159+
}.value
160+
}catch{
161+
throw.daemonStartFailure(error)
162+
}
163+
}
164+
101165
privatefunc connect()asyncthrows(DaemonError){
102166
guard client==nilelse{
103167
// Already connected
@@ -110,14 +174,17 @@ public class MutagenDaemon: FileSyncDaemon {
110174
transportSecurity:.plaintext,
111175
eventLoopGroup: group!
112176
)
113-
client=Daemon_DaemonAsyncClient(channel: channel!)
177+
client=DaemonClient(
178+
mgmt:Daemon_DaemonAsyncClient(channel: channel!),
179+
sync:Synchronization_SynchronizationAsyncClient(channel: channel!)
180+
)
114181
logger.info(
115182
"Successfully connected to mutagen daemon, socket:\(self.mutagenDaemonSocket.path, privacy:.public)"
116183
)
117184
}catch{
118185
logger.error("Failed to connect to gRPC:\(error)")
119186
try?awaitcleanupGRPC()
120-
throwDaemonError.connectionFailure(error)
187+
throw.connectionFailure(error)
121188
}
122189
}
123190

@@ -132,6 +199,10 @@ public class MutagenDaemon: FileSyncDaemon {
132199

133200
publicfunc stop()async{
134201
if case.unavailable= state{return}
202+
await transition.wait()
203+
defer{ transition.signal()}
204+
logger.info("stopping mutagen daemon")
205+
135206
state=.stopped
136207
guardFileManager.default.fileExists(atPath: mutagenDaemonSocket.path)else{
137208
// Already stopped
@@ -140,7 +211,7 @@ public class MutagenDaemon: FileSyncDaemon {
140211

141212
// "We don't check the response or error, because the daemon
142213
// may terminate before it has a chance to send the response."
143-
_=try?await client?.terminate(
214+
_=try?await client?.mgmt.terminate(
144215
Daemon_TerminateRequest(),
145216
callOptions:.init(timeLimit:.timeout(.milliseconds(500)))
146217
)
@@ -175,6 +246,7 @@ public class MutagenDaemon: FileSyncDaemon {
175246
"""
176247
)
177248
state=.failed(.terminatedUnexpectedly)
249+
return
178250
}
179251
}
180252

@@ -183,6 +255,55 @@ public class MutagenDaemon: FileSyncDaemon {
183255
logger.info("\(line, privacy:.public)")
184256
}
185257
}
258+
259+
publicfunc listSessions()asyncthrows->[FileSyncSession]{
260+
guard case.running= stateelse{
261+
return[]
262+
}
263+
// TODO: Implement
264+
return[]
265+
}
266+
267+
publicfunc createSession(with _:FileSyncSession)asyncthrows{
268+
if case.stopped= state{
269+
dothrows(DaemonError){
270+
tryawaitstart()
271+
} catch{
272+
state=.failed(error)
273+
return
274+
}
275+
}
276+
// TODO: Add Session
277+
}
278+
279+
publicfunc deleteSession()asyncthrows{
280+
// TODO: Delete session
281+
awaitstopIfNoSessions()
282+
}
283+
284+
privatefunc stopIfNoSessions()async{
285+
letsessions:Synchronization_ListResponse
286+
do{
287+
sessions=tryawait client!.sync.list(Synchronization_ListRequest.with{ reqin
288+
req.selection=.with{ selectionin
289+
selection.all=true
290+
}
291+
})
292+
}catch{
293+
state=.failed(.daemonStartFailure(error))
294+
return
295+
}
296+
// If there's no configured sessions, the daemon doesn't need to be running
297+
if sessions.sessionStates.isEmpty{
298+
logger.info("No sync sessions found")
299+
awaitstop()
300+
}
301+
}
302+
}
303+
304+
structDaemonClient{
305+
letmgmt:Daemon_DaemonAsyncClient
306+
letsync:Synchronization_SynchronizationAsyncClient
186307
}
187308

188309
publicenumDaemonState{
@@ -191,7 +312,7 @@ public enum DaemonState {
191312
case failed(DaemonError)
192313
case unavailable
193314

194-
vardescription:String{
315+
publicvardescription:String{
195316
switchself{
196317
case.running:
197318
"Running"
@@ -203,12 +324,27 @@ public enum DaemonState {
203324
"Unavailable"
204325
}
205326
}
327+
328+
publicvarcolor:Color{
329+
switchself{
330+
case.running:
331+
.green
332+
case.stopped:
333+
.gray
334+
case.failed:
335+
.red
336+
case.unavailable:
337+
.gray
338+
}
339+
}
206340
}
207341

208342
publicenumDaemonError:Error{
343+
case daemonNotRunning
209344
case daemonStartFailure(Error)
210345
case connectionFailure(Error)
211346
case terminatedUnexpectedly
347+
case grpcFailure(Error)
212348

213349
vardescription:String{
214350
switchself{
@@ -218,6 +354,10 @@ public enum DaemonError: Error {
218354
"Connection failure:\(error)"
219355
case.terminatedUnexpectedly:
220356
"Daemon terminated unexpectedly"
357+
case.daemonNotRunning:
358+
"The daemon must be started first"
359+
caselet.grpcFailure(error):
360+
"Failed to communicate with daemon:\(error)"
221361
}
222362
}
223363

‎Coder-Desktop/project.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ packages:
116116
exactVersion:1.24.2
117117
Subprocess:
118118
url:https://github.com/jamf/Subprocess
119-
revision:9d67b79
119+
revision:9d67b79
120+
Semaphore:
121+
url:https://github.com/groue/Semaphore/
122+
exactVersion:0.1.0
120123

121124
targets:
122125
Coder Desktop:
@@ -276,6 +279,7 @@ targets:
276279
product:SwiftProtobufPluginLibrary
277280
-package:GRPC
278281
-package:Subprocess
282+
-package:Semaphore
279283
-target:CoderSDK
280284
embed:false
281285

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp