From 0babf089269153ceade13ff21c81afc8fc14ebed Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 31 Dec 2025 16:28:51 +0100 Subject: [PATCH] chore: add mac app logging coverage --- .../Sources/Clawdis/ClawdisConfigFile.swift | 22 ++++++++++++++++--- .../Clawdis/GatewayLaunchAgentManager.swift | 12 +++++++++- .../Clawdis/GatewayProcessManager.swift | 16 ++++++++++++++ .../Sources/Clawdis/ModelCatalogLoader.swift | 11 ++++++++-- .../Sources/Clawdis/NotificationManager.swift | 17 ++++++++++++-- 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift index 4675badfa..abd420237 100644 --- a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift +++ b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift @@ -1,6 +1,8 @@ import Foundation enum ClawdisConfigFile { + private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "config") + static func url() -> URL { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".clawdis") @@ -15,8 +17,18 @@ enum ClawdisConfigFile { static func loadDict() -> [String: Any] { let url = self.url() - guard let data = try? Data(contentsOf: url) else { return [:] } - return (try? JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:] + guard FileManager.default.fileExists(atPath: url.path) else { return [:] } + do { + let data = try Data(contentsOf: url) + guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + self.logger.warning("config JSON root invalid") + return [:] + } + return root + } catch { + self.logger.warning("config read failed: \(error.localizedDescription)") + return [:] + } } static func saveDict(_ dict: [String: Any]) { @@ -27,7 +39,9 @@ enum ClawdisConfigFile { at: url.deletingLastPathComponent(), withIntermediateDirectories: true) try data.write(to: url, options: [.atomic]) - } catch {} + } catch { + self.logger.error("config save failed: \(error.localizedDescription)") + } } static func loadGatewayDict() -> [String: Any] { @@ -59,6 +73,7 @@ enum ClawdisConfigFile { browser["enabled"] = enabled root["browser"] = browser self.saveDict(root) + self.logger.debug("browser control updated enabled=\(enabled)") } static func agentWorkspace() -> String? { @@ -78,5 +93,6 @@ enum ClawdisConfigFile { } root["agent"] = agent self.saveDict(root) + self.logger.debug("agent workspace updated set=\(!trimmed.isEmpty)") } } diff --git a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift index 60b4afb0f..70de79ecd 100644 --- a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift @@ -1,6 +1,7 @@ import Foundation enum GatewayLaunchAgentManager { + private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "gateway.launchd") private static let supportedBindModes: Set = ["loopback", "tailnet", "lan", "auto"] private static var plistURL: URL { @@ -26,12 +27,16 @@ enum GatewayLaunchAgentManager { if enabled { let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath) guard FileManager.default.isExecutableFile(atPath: gatewayBin) else { + self.logger.error("launchd enable failed: gateway missing at \(gatewayBin)") return "Embedded gateway missing in bundle; rebuild via scripts/package-mac-app.sh" } + self.logger.info("launchd enable requested port=\(port)") self.writePlist(bundlePath: bundlePath, port: port) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(gatewayLaunchdLabel)"]) let bootstrap = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path]) if bootstrap.status != 0 { + let msg = bootstrap.output.trimmingCharacters(in: .whitespacesAndNewlines) + self.logger.error("launchd bootstrap failed: \(msg)") return bootstrap.output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Failed to bootstrap gateway launchd job" : bootstrap.output.trimmingCharacters(in: .whitespacesAndNewlines) @@ -42,6 +47,7 @@ enum GatewayLaunchAgentManager { return nil } + self.logger.info("launchd disable requested") _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(gatewayLaunchdLabel)"]) try? FileManager.default.removeItem(at: self.plistURL) return nil @@ -103,7 +109,11 @@ enum GatewayLaunchAgentManager { """ - try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8) + do { + try plist.write(to: self.plistURL, atomically: true, encoding: .utf8) + } catch { + self.logger.error("launchd plist write failed: \(error.localizedDescription)") + } } private static func preferredGatewayBind() -> String? { diff --git a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift index 0b4a87c9f..cca6cf79b 100644 --- a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift @@ -42,6 +42,7 @@ final class GatewayProcessManager { private var environmentRefreshTask: Task? private var lastEnvironmentRefresh: Date? private var logRefreshTask: Task? + private let logger = Logger(subsystem: "com.steipete.clawdis", category: "gateway.process") private let logLimit = 20000 // characters to keep in-memory private let environmentRefreshMinInterval: TimeInterval = 30 @@ -53,8 +54,10 @@ final class GatewayProcessManager { self.stop() self.status = .stopped self.appendLog("[gateway] remote mode active; skipping local gateway\n") + self.logger.info("gateway process skipped: remote mode active") return } + self.logger.debug("gateway active requested active=\(active)") self.desiredActive = active self.refreshEnvironmentStatus() if active { @@ -86,6 +89,7 @@ final class GatewayProcessManager { return } self.status = .starting + self.logger.debug("gateway start requested") // First try to latch onto an already-running gateway to avoid spawning a duplicate. Task { [weak self] in @@ -98,6 +102,7 @@ final class GatewayProcessManager { await MainActor.run { self.status = .failed("Attach-only enabled; no gateway to attach") self.appendLog("[gateway] attach-only enabled; not spawning local gateway\n") + self.logger.warning("gateway attach-only enabled; not spawning") } return } @@ -110,6 +115,7 @@ final class GatewayProcessManager { self.existingGatewayDetails = nil self.lastFailureReason = nil self.status = .stopped + self.logger.info("gateway stop requested") let bundlePath = Bundle.main.bundleURL.path Task { _ = await GatewayLaunchAgentManager.set( @@ -182,6 +188,7 @@ final class GatewayProcessManager { self.existingGatewayDetails = details self.status = .attachedExisting(details: details) self.appendLog("[gateway] using existing instance: \(details)\n") + self.logger.info("gateway using existing instance details=\(details)") self.refreshControlChannelIfNeeded(reason: "attach existing") self.refreshLog() return true @@ -197,6 +204,7 @@ final class GatewayProcessManager { self.status = .failed(reason) self.lastFailureReason = reason self.appendLog("[gateway] existing listener on port \(port) but attach failed: \(reason)\n") + self.logger.warning("gateway attach failed reason=\(reason)") return true } @@ -268,16 +276,19 @@ final class GatewayProcessManager { await MainActor.run { self.status = .failed(resolution.status.message) } + self.logger.error("gateway command resolve failed: \(resolution.status.message)") return } let bundlePath = Bundle.main.bundleURL.path let port = GatewayEnvironment.gatewayPort() self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n") + self.logger.info("gateway enabling launchd port=\(port)") let err = await GatewayLaunchAgentManager.set(enabled: true, bundlePath: bundlePath, port: port) if let err { self.status = .failed(err) self.lastFailureReason = err + self.logger.error("gateway launchd enable failed: \(err)") return } @@ -290,6 +301,7 @@ final class GatewayProcessManager { let instance = await PortGuardian.shared.describe(port: port) let details = instance.map { "pid \($0.pid)" } self.status = .running(details: details) + self.logger.info("gateway started details=\(details ?? "ok")") self.refreshControlChannelIfNeeded(reason: "gateway started") self.refreshLog() return @@ -300,6 +312,7 @@ final class GatewayProcessManager { self.status = .failed("Gateway did not start in time") self.lastFailureReason = "launchd start timeout" + self.logger.warning("gateway start timed out") } private func appendLog(_ chunk: String) { @@ -317,6 +330,7 @@ final class GatewayProcessManager { break } self.appendLog("[gateway] refreshing control channel (\(reason))\n") + self.logger.debug("gateway control channel refresh reason=\(reason)") Task { await ControlChannel.shared.configure() } } @@ -332,12 +346,14 @@ final class GatewayProcessManager { } } self.appendLog("[gateway] readiness wait timed out\n") + self.logger.warning("gateway readiness wait timed out") return false } func clearLog() { self.log = "" try? FileManager.default.removeItem(atPath: LogLocator.launchdGatewayLogPath) + self.logger.debug("gateway log cleared") } func setProjectRoot(path: String) { diff --git a/apps/macos/Sources/Clawdis/ModelCatalogLoader.swift b/apps/macos/Sources/Clawdis/ModelCatalogLoader.swift index ebd5c6e97..61c234996 100644 --- a/apps/macos/Sources/Clawdis/ModelCatalogLoader.swift +++ b/apps/macos/Sources/Clawdis/ModelCatalogLoader.swift @@ -4,18 +4,23 @@ import JavaScriptCore enum ModelCatalogLoader { static let defaultPath: String = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path + private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "models") static func load(from path: String) async throws -> [ModelChoice] { let expanded = (path as NSString).expandingTildeInPath + self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: expanded).lastPathComponent)") let source = try String(contentsOfFile: expanded, encoding: .utf8) let sanitized = self.sanitize(source: source) let ctx = JSContext() ctx?.exceptionHandler = { _, exception in - if let exception { print("JS exception: \(exception)") } + if let exception { + self.logger.warning("model catalog JS exception: \(exception)") + } } ctx?.evaluateScript(sanitized) guard let rawModels = ctx?.objectForKeyedSubscript("MODELS")?.toDictionary() as? [String: Any] else { + self.logger.error("model catalog parse failed: MODELS missing") throw NSError( domain: "ModelCatalogLoader", code: 1, @@ -33,12 +38,14 @@ enum ModelCatalogLoader { } } - return choices.sorted { lhs, rhs in + let sorted = choices.sorted { lhs, rhs in if lhs.provider == rhs.provider { return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending } return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending } + self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)") + return sorted } private static func sanitize(source: String) -> String { diff --git a/apps/macos/Sources/Clawdis/NotificationManager.swift b/apps/macos/Sources/Clawdis/NotificationManager.swift index 4650d3afe..e4095ab2e 100644 --- a/apps/macos/Sources/Clawdis/NotificationManager.swift +++ b/apps/macos/Sources/Clawdis/NotificationManager.swift @@ -5,6 +5,8 @@ import UserNotifications @MainActor struct NotificationManager { + private let logger = Logger(subsystem: "com.steipete.clawdis", category: "notifications") + private static let hasTimeSensitiveEntitlement: Bool = { guard let task = SecTaskCreateFromSelf(nil) else { return false } let key = "com.apple.developer.usernotifications.time-sensitive" as CFString @@ -17,8 +19,12 @@ struct NotificationManager { let status = await center.notificationSettings() if status.authorizationStatus == .notDetermined { let granted = try? await center.requestAuthorization(options: [.alert, .sound, .badge]) - if granted != true { return false } + if granted != true { + self.logger.warning("notification permission denied (request)") + return false + } } else if status.authorizationStatus != .authorized { + self.logger.warning("notification permission denied status=\(status.authorizationStatus.rawValue)") return false } @@ -37,15 +43,22 @@ struct NotificationManager { case .active: content.interruptionLevel = .active case .timeSensitive: - content.interruptionLevel = Self.hasTimeSensitiveEntitlement ? .timeSensitive : .active + if Self.hasTimeSensitiveEntitlement { + content.interruptionLevel = .timeSensitive + } else { + self.logger.debug("time-sensitive notification requested without entitlement; falling back to active") + content.interruptionLevel = .active + } } } let req = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) do { try await center.add(req) + self.logger.debug("notification queued") return true } catch { + self.logger.error("notification send failed: \(error.localizedDescription)") return false } }