perf(mac): move blocking launchctl/webchat work off main
parent
a19d4c19d3
commit
73211c900b
|
|
@ -138,7 +138,7 @@ final class AppState: ObservableObject {
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
|
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
|
||||||
self.launchAtLogin = LaunchAgentManager.status()
|
self.launchAtLogin = false
|
||||||
self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen")
|
self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen")
|
||||||
self.debugPaneEnabled = UserDefaults.standard.bool(forKey: "clawdis.debugPaneEnabled")
|
self.debugPaneEnabled = UserDefaults.standard.bool(forKey: "clawdis.debugPaneEnabled")
|
||||||
let savedVoiceWake = UserDefaults.standard.bool(forKey: swabbleEnabledKey)
|
let savedVoiceWake = UserDefaults.standard.bool(forKey: swabbleEnabledKey)
|
||||||
|
|
@ -189,6 +189,11 @@ final class AppState: ObservableObject {
|
||||||
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
|
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
|
||||||
self.webChatPort = storedPort > 0 ? storedPort : 18788
|
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() {
|
if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
|
||||||
self.swabbleEnabled = false
|
self.swabbleEnabled = false
|
||||||
}
|
}
|
||||||
|
|
@ -248,7 +253,9 @@ enum AppStateStore {
|
||||||
static var isPausedFlag: Bool { UserDefaults.standard.bool(forKey: pauseDefaultsKey) }
|
static var isPausedFlag: Bool { UserDefaults.standard.bool(forKey: pauseDefaultsKey) }
|
||||||
|
|
||||||
static func updateLaunchAtLogin(enabled: Bool) {
|
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 {
|
static var webChatEnabled: Bool {
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,18 @@ enum LaunchAgentManager {
|
||||||
.appendingPathComponent("Library/LaunchAgents/com.steipete.clawdis.plist")
|
.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 }
|
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
|
return result == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static func set(enabled: Bool, bundlePath: String) {
|
static func set(enabled: Bool, bundlePath: String) async {
|
||||||
if enabled {
|
if enabled {
|
||||||
self.writePlist(bundlePath: bundlePath)
|
self.writePlist(bundlePath: bundlePath)
|
||||||
_ = self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
|
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
|
||||||
_ = self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
|
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
|
||||||
_ = self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
|
_ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
|
||||||
} else {
|
} else {
|
||||||
// Disable autostart going forward but leave the current app running.
|
// 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).
|
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
|
||||||
|
|
@ -84,15 +84,21 @@ enum LaunchAgentManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private static func runLaunchctl(_ args: [String]) -> Int32 {
|
private static func runLaunchctl(_ args: [String]) async -> Int32 {
|
||||||
|
await Task.detached(priority: .utility) { () -> Int32 in
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.launchPath = "/bin/launchctl"
|
process.launchPath = "/bin/launchctl"
|
||||||
process.arguments = args
|
process.arguments = args
|
||||||
process.standardOutput = Pipe()
|
process.standardOutput = Pipe()
|
||||||
process.standardError = Pipe()
|
process.standardError = Pipe()
|
||||||
try? process.run()
|
do {
|
||||||
|
try process.run()
|
||||||
process.waitUntilExit()
|
process.waitUntilExit()
|
||||||
return process.terminationStatus
|
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"])
|
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] = [
|
var args: [String] = [
|
||||||
"-o", "BatchMode=yes",
|
"-o", "BatchMode=yes",
|
||||||
"-o", "IdentitiesOnly=yes",
|
"-o", "IdentitiesOnly=yes",
|
||||||
|
|
@ -250,19 +250,35 @@ final class WebChatTunnel {
|
||||||
return WebChatTunnel(process: process, localPort: localPort)
|
return WebChatTunnel(process: process, localPort: localPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func findPort(preferred: UInt16?) throws -> UInt16 {
|
private static func findPort(preferred: UInt16?) async throws -> UInt16 {
|
||||||
if let preferred {
|
if let preferred, Self.portIsFree(preferred) { return preferred }
|
||||||
if 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)
|
let listener = try NWListener(using: .tcp, on: .any)
|
||||||
listener.start(queue: .main)
|
listener.newConnectionHandler = { connection in connection.cancel() }
|
||||||
while listener.port == nil {
|
listener.stateUpdateHandler = { state in
|
||||||
RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.05))
|
switch state {
|
||||||
}
|
case .ready:
|
||||||
let port = listener.port?.rawValue
|
if let port = listener.port?.rawValue {
|
||||||
|
listener.stateUpdateHandler = nil
|
||||||
listener.cancel()
|
listener.cancel()
|
||||||
guard let port else { throw NSError(domain: "WebChat", code: 4, userInfo: [NSLocalizedDescriptionKey: "no port"])}
|
cont.resume(returning: port)
|
||||||
return 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func portIsFree(_ port: UInt16) -> Bool {
|
private static func portIsFree(_ port: UInt16) -> Bool {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue