perf(mac): move blocking launchctl/webchat work off main
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user