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

Commit6f6049e

Browse files
chore: manage mutagen daemon lifecycle (#98)
Closescoder/internal#381.- Moves the VPN-specific app files into a `VPN` folder.- Adds an empty `Resources` folder whose contents are copied into the bundle at build time.- Adds a `MutagenDaemon` abstraction for managing the mutagen daemon lifecycle, this class: - Starts the mutagen daemon using `mutagen daemon run`, with a `MUTAGEN_DATA_DIRECTORY` in `Application Support/Coder Desktop/Mutagen`, to avoid collisions with a system mutagen using `~/.mutagen`. - Maintains a `gRPC` connection to the daemon socket. - Stops the mutagen daemon over `gRPC` - Relays stdout & stderr from the daemon, and watches if the process exits unexpectedly. - Handles replacing an orphaned `mutagen daemon run` process if one exists.This PR does not embed the mutagen binaries within the bundle, it just handles the case where they're present.## Why is the file sync code in VPNLib?When I had the FileSync code (namely protobuf definitions) in either:- The app target- A new `FSLib` framework targetEither the network extension crashed (in the first case) or the app crashed (in the second case) on launch.The crash was super obtuse:```Library not loaded: @rpath/SwiftProtobuf.framework/Versions/A/SwiftProtobuf```especially considering `SwiftProtobuf` doesn't have a stable ABI and shouldn't be compiled as a framework.At least one other person has ran into this issue when importing `SwiftProtobuf` multiple times:apple/swift-protobuf#1506 (comment)Curiously, this also wasn't happening on local development builds (building and running via the XCode GUI), only when exporting via our build script.### SolutionWe're just going to overload `VPNLib` as the source of all our SwiftProtobuf & GRPC code. Since it's pretty big, and we don't want to embed it twice, we'll embed it once within the System Extension, and then have the app look for it in that bundle, see `LD_RUNPATH_SEARCH_PATHS`. It's not exactly ideal, but I don't think it's worth going to war with XCode over.#### TODO- [x] Replace the `Process` withhttps://github.com/jamf/Subprocess
1 parent2094e9f commit6f6049e

17 files changed

+746
-9
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: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import FluidMenuBarExtra
22
import NetworkExtension
33
import SwiftUI
4+
import VPNLib
45

56
@main
67
structDesktopApp:App{
@@ -30,10 +31,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3031
privatevarmenuBar:MenuBarController?
3132
letvpn:CoderVPNService
3233
letstate:AppState
34+
letfileSyncDaemon:MutagenDaemon
3335

3436
overrideinit(){
3537
vpn=CoderVPNService()
3638
state=AppState(onChange: vpn.configureTunnelProviderProtocol)
39+
fileSyncDaemon=MutagenDaemon()
3740
}
3841

3942
func applicationDidFinishLaunching(_:Notification){
@@ -56,14 +59,23 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5659
state.reconfigure()
5760
}
5861
}
62+
// TODO: Start the daemon only once a file sync is configured
63+
Task{
64+
await fileSyncDaemon.start()
65+
}
5966
}
6067

6168
// This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
6269
// or return `.terminateNow`
6370
func applicationShouldTerminate(_:NSApplication)->NSApplication.TerminateReply{
64-
if !state.stopVPNOnQuit{return.terminateNow}
6571
Task{
66-
await vpn.stop()
72+
asyncletvpnTask:Void={
73+
ifawaitself.state.stopVPNOnQuit{
74+
awaitself.vpn.stop()
75+
}
76+
}()
77+
asyncletfileSyncTask:Void=self.fileSyncDaemon.stop()
78+
_=await(vpnTask, fileSyncTask)
6779
NSApp.reply(toApplicationShouldTerminate:true)
6880
}
6981
return.terminateLater

‎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
classAppState:ObservableObject{
89
letappId=Bundle.main.bundleIdentifier!
910

‎Coder Desktop/Resources/.gitkeep

Whitespace-only changes.
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import Foundation
2+
import GRPC
3+
import NIO
4+
import os
5+
import Subprocess
6+
7+
@MainActor
8+
publicprotocolFileSyncDaemon:ObservableObject{
9+
varstate:DaemonState{get}
10+
func start()async
11+
func stop()async
12+
}
13+
14+
@MainActor
15+
publicclassMutagenDaemon:FileSyncDaemon{
16+
privateletlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"mutagen")
17+
18+
@Publishedpublicvarstate:DaemonState=.stopped{
19+
didSet{
20+
logger.info("daemon state changed:\(self.state.description, privacy:.public)")
21+
}
22+
}
23+
24+
privatevarmutagenProcess:Subprocess?
25+
privateletmutagenPath:URL!
26+
privateletmutagenDataDirectory:URL
27+
privateletmutagenDaemonSocket:URL
28+
29+
privatevargroup:MultiThreadedEventLoopGroup?
30+
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")
45+
mutagenDaemonSocket= mutagenDataDirectory.appending(path:"daemon").appending(path:"daemon.sock")
46+
// It shouldn't be fatal if the app was built without Mutagen embedded,
47+
// but file sync will be unavailable.
48+
if mutagenPath==nil{
49+
logger.warning("Mutagen not embedded in app, file sync will be unavailable")
50+
state=.unavailable
51+
}
52+
}
53+
54+
publicfunc start()async{
55+
if case.unavailable= state{return}
56+
57+
// Stop an orphaned daemon, if there is one
58+
try?awaitconnect()
59+
awaitstop()
60+
61+
mutagenProcess=createMutagenProcess()
62+
// swiftlint:disable:next large_tuple
63+
let(standardOutput, standardError, waitForExit):(Pipe.AsyncBytes,Pipe.AsyncBytes,@Sendable()async->Void)
64+
do{
65+
(standardOutput, standardError, waitForExit)=try mutagenProcess!.run()
66+
}catch{
67+
state=.failed(DaemonError.daemonStartFailure(error))
68+
return
69+
}
70+
71+
Task{
72+
awaitstreamHandler(io: standardOutput)
73+
logger.info("standard output stream closed")
74+
}
75+
76+
Task{
77+
awaitstreamHandler(io: standardError)
78+
logger.info("standard error stream closed")
79+
}
80+
81+
Task{
82+
awaitterminationHandler(waitForExit: waitForExit)
83+
}
84+
85+
do{
86+
tryawaitconnect()
87+
}catch{
88+
state=.failed(DaemonError.daemonStartFailure(error))
89+
return
90+
}
91+
92+
state=.running
93+
logger.info(
94+
"""
95+
mutagen daemon started, pid:
96+
\(self.mutagenProcess?.pid.description??"unknown", privacy:.public)
97+
"""
98+
)
99+
}
100+
101+
privatefunc connect()asyncthrows(DaemonError){
102+
guard client==nilelse{
103+
// Already connected
104+
return
105+
}
106+
group=MultiThreadedEventLoopGroup(numberOfThreads:1)
107+
do{
108+
channel=tryGRPCChannelPool.with(
109+
target:.unixDomainSocket(mutagenDaemonSocket.path),
110+
transportSecurity:.plaintext,
111+
eventLoopGroup: group!
112+
)
113+
client=Daemon_DaemonAsyncClient(channel: channel!)
114+
logger.info(
115+
"Successfully connected to mutagen daemon, socket:\(self.mutagenDaemonSocket.path, privacy:.public)"
116+
)
117+
}catch{
118+
logger.error("Failed to connect to gRPC:\(error)")
119+
try?awaitcleanupGRPC()
120+
throwDaemonError.connectionFailure(error)
121+
}
122+
}
123+
124+
privatefunc cleanupGRPC()asyncthrows{
125+
try?await channel?.close().get()
126+
try?await group?.shutdownGracefully()
127+
128+
client=nil
129+
channel=nil
130+
group=nil
131+
}
132+
133+
publicfunc stop()async{
134+
if case.unavailable= state{return}
135+
state=.stopped
136+
guardFileManager.default.fileExists(atPath: mutagenDaemonSocket.path)else{
137+
// Already stopped
138+
return
139+
}
140+
141+
// "We don't check the response or error, because the daemon
142+
// may terminate before it has a chance to send the response."
143+
_=try?await client?.terminate(
144+
Daemon_TerminateRequest(),
145+
callOptions:.init(timeLimit:.timeout(.milliseconds(500)))
146+
)
147+
148+
try?awaitcleanupGRPC()
149+
150+
mutagenProcess?.kill()
151+
mutagenProcess=nil
152+
logger.info("Daemon stopped and gRPC connection closed")
153+
}
154+
155+
privatefunc createMutagenProcess()->Subprocess{
156+
letprocess=Subprocess([mutagenPath.path,"daemon","run"])
157+
process.environment=[
158+
"MUTAGEN_DATA_DIRECTORY": mutagenDataDirectory.path,
159+
]
160+
logger.info("setting mutagen data directory:\(self.mutagenDataDirectory.path, privacy:.public)")
161+
return process
162+
}
163+
164+
privatefunc terminationHandler(waitForExit:@Sendable()async->Void)async{
165+
awaitwaitForExit()
166+
167+
switch state{
168+
case.stopped:
169+
logger.info("mutagen daemon stopped")
170+
default:
171+
logger.error(
172+
"""
173+
mutagen daemon exited unexpectedly with code:
174+
\(self.mutagenProcess?.exitCode.description??"unknown")
175+
"""
176+
)
177+
state=.failed(.terminatedUnexpectedly)
178+
}
179+
}
180+
181+
privatefunc streamHandler(io:Pipe.AsyncBytes)async{
182+
forawaitlinein io.lines{
183+
logger.info("\(line, privacy:.public)")
184+
}
185+
}
186+
}
187+
188+
publicenumDaemonState{
189+
case running
190+
case stopped
191+
case failed(DaemonError)
192+
case unavailable
193+
194+
vardescription:String{
195+
switchself{
196+
case.running:
197+
"Running"
198+
case.stopped:
199+
"Stopped"
200+
caselet.failed(error):
201+
"Failed:\(error)"
202+
case.unavailable:
203+
"Unavailable"
204+
}
205+
}
206+
}
207+
208+
publicenumDaemonError:Error{
209+
case daemonStartFailure(Error)
210+
case connectionFailure(Error)
211+
case terminatedUnexpectedly
212+
213+
vardescription:String{
214+
switchself{
215+
caselet.daemonStartFailure(error):
216+
"Daemon start failure:\(error)"
217+
caselet.connectionFailure(error):
218+
"Connection failure:\(error)"
219+
case.terminatedUnexpectedly:
220+
"Daemon terminated unexpectedly"
221+
}
222+
}
223+
224+
varlocalizedDescription:String{ description}
225+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp