perf(mac): move blocking launchctl/webchat work off main

This commit is contained in:
Peter Steinberger
2025-12-08 18:42:13 +01:00
parent a19d4c19d3
commit 73211c900b
3 changed files with 59 additions and 30 deletions

View File

@@ -138,7 +138,7 @@ final class AppState: ObservableObject {
init() {
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
self.launchAtLogin = LaunchAgentManager.status()
self.launchAtLogin = false
self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen")
self.debugPaneEnabled = UserDefaults.standard.bool(forKey: "clawdis.debugPaneEnabled")
let savedVoiceWake = UserDefaults.standard.bool(forKey: swabbleEnabledKey)
@@ -189,6 +189,11 @@ final class AppState: ObservableObject {
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
self.webChatPort = storedPort > 0 ? storedPort : 18788
Task.detached(priority: .utility) { [weak self] in
let current = await LaunchAgentManager.status()
await MainActor.run { [weak self] in self?.launchAtLogin = current }
}
if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = false
}
@@ -248,7 +253,9 @@ enum AppStateStore {
static var isPausedFlag: Bool { UserDefaults.standard.bool(forKey: pauseDefaultsKey) }
static func updateLaunchAtLogin(enabled: Bool) {
LaunchAgentManager.set(enabled: enabled, bundlePath: Bundle.main.bundlePath)
Task.detached(priority: .utility) {
await LaunchAgentManager.set(enabled: enabled, bundlePath: Bundle.main.bundlePath)
}
}
static var webChatEnabled: Bool {

View File

@@ -26,18 +26,18 @@ enum LaunchAgentManager {
.appendingPathComponent("Library/LaunchAgents/com.steipete.clawdis.plist")
}
static func status() -> Bool {
static func status() async -> Bool {
guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false }
let result = self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
return result == 0
}
static func set(enabled: Bool, bundlePath: String) {
static func set(enabled: Bool, bundlePath: String) async {
if enabled {
self.writePlist(bundlePath: bundlePath)
_ = self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
_ = self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
_ = self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
_ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
} else {
// Disable autostart going forward but leave the current app running.
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
@@ -84,15 +84,21 @@ enum LaunchAgentManager {
}
@discardableResult
private static func runLaunchctl(_ args: [String]) -> Int32 {
let process = Process()
process.launchPath = "/bin/launchctl"
process.arguments = args
process.standardOutput = Pipe()
process.standardError = Pipe()
try? process.run()
process.waitUntilExit()
return process.terminationStatus
private static func runLaunchctl(_ args: [String]) async -> Int32 {
await Task.detached(priority: .utility) { () -> Int32 in
let process = Process()
process.launchPath = "/bin/launchctl"
process.arguments = args
process.standardOutput = Pipe()
process.standardError = Pipe()
do {
try process.run()
process.waitUntilExit()
return process.terminationStatus
} catch {
return -1
}
}.value
}
}

View File

@@ -217,7 +217,7 @@ final class WebChatTunnel {
throw NSError(domain: "WebChat", code: 3, userInfo: [NSLocalizedDescriptionKey: "remote not configured"])
}
let localPort = try Self.findPort(preferred: preferredLocalPort)
let localPort = try await Self.findPort(preferred: preferredLocalPort)
var args: [String] = [
"-o", "BatchMode=yes",
"-o", "IdentitiesOnly=yes",
@@ -250,19 +250,35 @@ final class WebChatTunnel {
return WebChatTunnel(process: process, localPort: localPort)
}
private static func findPort(preferred: UInt16?) throws -> UInt16 {
if let preferred {
if Self.portIsFree(preferred) { return preferred }
private static func findPort(preferred: UInt16?) async throws -> UInt16 {
if let preferred, Self.portIsFree(preferred) { return preferred }
return try await withCheckedThrowingContinuation { cont in
let queue = DispatchQueue(label: "com.steipete.clawdis.webchat.port", qos: .utility)
do {
let listener = try NWListener(using: .tcp, on: .any)
listener.newConnectionHandler = { connection in connection.cancel() }
listener.stateUpdateHandler = { state in
switch state {
case .ready:
if let port = listener.port?.rawValue {
listener.stateUpdateHandler = nil
listener.cancel()
cont.resume(returning: port)
}
case let .failed(error):
listener.stateUpdateHandler = nil
listener.cancel()
cont.resume(throwing: error)
default:
break
}
}
listener.start(queue: queue)
} catch {
cont.resume(throwing: error)
}
}
let listener = try NWListener(using: .tcp, on: .any)
listener.start(queue: .main)
while listener.port == nil {
RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
}
let port = listener.port?.rawValue
listener.cancel()
guard let port else { throw NSError(domain: "WebChat", code: 4, userInfo: [NSLocalizedDescriptionKey: "no port"])}
return port
}
private static func portIsFree(_ port: UInt16) -> Bool {