- Notifications
You must be signed in to change notification settings - Fork5
chore: manage mutagen daemon lifecycle#98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
0f362f8
513ccd8
3291e73
9be6173
b0cbab8
ebcadbe
5ed3893
c1947aa
b13a44f
c9cba6d
2b673b8
76abed5
f2fc365
21bb169
2bf41aa
16a7263
382fd9b
ad1b24d
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# TODO: Remove this once the grpc-swift-protobuf generator adds a lint disable comment | ||
excluded: | ||
- "**/*.pb.swift" | ||
- "**/*.grpc.swift" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
--selfrequired log,info,error,debug,critical,fault | ||
--exclude **.pb.swift,**.grpc.swift | ||
--condassignment always |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -4,6 +4,7 @@ import KeychainAccess | ||
import NetworkExtension | ||
import SwiftUI | ||
@MainActor | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. drive-by fix: I believe this is implicit in the | ||
class AppState: ObservableObject { | ||
let appId = Bundle.main.bundleIdentifier! | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
import Foundation | ||
import GRPC | ||
import NIO | ||
import os | ||
import Subprocess | ||
@MainActor | ||
publicprotocolFileSyncDaemon:ObservableObject{ | ||
varstate:DaemonState{get} | ||
func start()async | ||
func stop()async | ||
} | ||
@MainActor | ||
publicclassMutagenDaemon:FileSyncDaemon{ | ||
privateletlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"mutagen") | ||
@Publishedpublicvarstate:DaemonState=.stopped{ | ||
didSet{ | ||
logger.info("daemon state changed:\(self.state.description, privacy:.public)") | ||
} | ||
} | ||
privatevarmutagenProcess:Subprocess? | ||
privateletmutagenPath:URL! | ||
privateletmutagenDataDirectory:URL | ||
privateletmutagenDaemonSocket:URL | ||
privatevargroup:MultiThreadedEventLoopGroup? | ||
privatevarchannel:GRPCChannel? | ||
privatevarclient:Daemon_DaemonAsyncClient? | ||
publicinit(){ | ||
#if arch(arm64) | ||
mutagenPath=Bundle.main.url(forResource:"mutagen-darwin-arm64", withExtension:nil) | ||
#elseif arch(x86_64) | ||
mutagenPath=Bundle.main.url(forResource:"mutagen-darwin-amd64", withExtension:nil) | ||
#else | ||
fatalError("unknown architecture") | ||
#endif | ||
mutagenDataDirectory=FileManager.default.urls( | ||
for:.applicationSupportDirectory, | ||
in:.userDomainMask | ||
).first!.appending(path:"Coder Desktop").appending(path:"Mutagen") | ||
mutagenDaemonSocket= mutagenDataDirectory.appending(path:"daemon").appending(path:"daemon.sock") | ||
// It shouldn't be fatal if the app was built without Mutagen embedded, | ||
// but file sync will be unavailable. | ||
if mutagenPath==nil{ | ||
logger.warning("Mutagen not embedded in app, file sync will be unavailable") | ||
state=.unavailable | ||
} | ||
} | ||
publicfunc start()async{ | ||
if case.unavailable= state{return} | ||
ThomasK33 marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
// Stop an orphaned daemon, if there is one | ||
try?awaitconnect() | ||
awaitstop() | ||
mutagenProcess=createMutagenProcess() | ||
// swiftlint:disable:next large_tuple | ||
let(standardOutput, standardError, waitForExit):(Pipe.AsyncBytes,Pipe.AsyncBytes,@Sendable()async->Void) | ||
do{ | ||
(standardOutput, standardError, waitForExit)=try mutagenProcess!.run() | ||
}catch{ | ||
state=.failed(DaemonError.daemonStartFailure(error)) | ||
return | ||
} | ||
Task{ | ||
awaitstreamHandler(io: standardOutput) | ||
logger.info("standard output stream closed") | ||
} | ||
Task{ | ||
awaitstreamHandler(io: standardError) | ||
logger.info("standard error stream closed") | ||
} | ||
Task{ | ||
awaitterminationHandler(waitForExit: waitForExit) | ||
} | ||
do{ | ||
tryawaitconnect() | ||
}catch{ | ||
state=.failed(DaemonError.daemonStartFailure(error)) | ||
return | ||
} | ||
state=.running | ||
logger.info( | ||
""" | ||
mutagen daemon started, pid: | ||
\(self.mutagenProcess?.pid.description??"unknown", privacy:.public) | ||
""" | ||
) | ||
} | ||
privatefunc connect()asyncthrows(DaemonError){ | ||
guard client==nilelse{ | ||
// Already connected | ||
return | ||
} | ||
group=MultiThreadedEventLoopGroup(numberOfThreads:1) | ||
do{ | ||
channel=tryGRPCChannelPool.with( | ||
target:.unixDomainSocket(mutagenDaemonSocket.path), | ||
transportSecurity:.plaintext, | ||
eventLoopGroup: group! | ||
) | ||
client=Daemon_DaemonAsyncClient(channel: channel!) | ||
logger.info( | ||
"Successfully connected to mutagen daemon, socket:\(self.mutagenDaemonSocket.path, privacy:.public)" | ||
) | ||
}catch{ | ||
logger.error("Failed to connect to gRPC:\(error)") | ||
try?awaitcleanupGRPC() | ||
throwDaemonError.connectionFailure(error) | ||
} | ||
} | ||
privatefunc cleanupGRPC()asyncthrows{ | ||
try?await channel?.close().get() | ||
try?await group?.shutdownGracefully() | ||
client=nil | ||
channel=nil | ||
group=nil | ||
} | ||
publicfunc stop()async{ | ||
if case.unavailable= state{return} | ||
state=.stopped | ||
guardFileManager.default.fileExists(atPath: mutagenDaemonSocket.path)else{ | ||
// Already stopped | ||
return | ||
} | ||
// "We don't check the response or error, because the daemon | ||
// may terminate before it has a chance to send the response." | ||
_=try?await client?.terminate( | ||
Daemon_TerminateRequest(), | ||
callOptions:.init(timeLimit:.timeout(.milliseconds(500))) | ||
) | ||
try?awaitcleanupGRPC() | ||
mutagenProcess?.kill() | ||
mutagenProcess=nil | ||
logger.info("Daemon stopped and gRPC connection closed") | ||
} | ||
privatefunc createMutagenProcess()->Subprocess{ | ||
letprocess=Subprocess([mutagenPath.path,"daemon","run"]) | ||
process.environment=[ | ||
"MUTAGEN_DATA_DIRECTORY": mutagenDataDirectory.path, | ||
] | ||
logger.info("setting mutagen data directory:\(self.mutagenDataDirectory.path, privacy:.public)") | ||
return process | ||
} | ||
privatefunc terminationHandler(waitForExit:@Sendable()async->Void)async{ | ||
awaitwaitForExit() | ||
switch state{ | ||
case.stopped: | ||
logger.info("mutagen daemon stopped") | ||
default: | ||
logger.error( | ||
""" | ||
mutagen daemon exited unexpectedly with code: | ||
\(self.mutagenProcess?.exitCode.description??"unknown") | ||
""" | ||
) | ||
state=.failed(.terminatedUnexpectedly) | ||
} | ||
} | ||
privatefunc streamHandler(io:Pipe.AsyncBytes)async{ | ||
forawaitlinein io.lines{ | ||
logger.info("\(line, privacy:.public)") | ||
} | ||
} | ||
} | ||
publicenumDaemonState{ | ||
case running | ||
case stopped | ||
case failed(DaemonError) | ||
case unavailable | ||
vardescription:String{ | ||
switchself{ | ||
case.running: | ||
"Running" | ||
case.stopped: | ||
"Stopped" | ||
caselet.failed(error): | ||
"Failed:\(error)" | ||
case.unavailable: | ||
"Unavailable" | ||
} | ||
} | ||
} | ||
publicenumDaemonError:Error{ | ||
case daemonStartFailure(Error) | ||
case connectionFailure(Error) | ||
case terminatedUnexpectedly | ||
vardescription:String{ | ||
switchself{ | ||
caselet.daemonStartFailure(error): | ||
"Daemon start failure:\(error)" | ||
caselet.connectionFailure(error): | ||
"Connection failure:\(error)" | ||
case.terminatedUnexpectedly: | ||
"Daemon terminated unexpectedly" | ||
} | ||
} | ||
varlocalizedDescription:String{ description} | ||
} |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.