diff --git a/Sources/ProcessBarMonitor/LegacyLaunchAgentCleaner.swift b/Sources/ProcessBarMonitor/LegacyLaunchAgentCleaner.swift new file mode 100644 index 0000000..d195a19 --- /dev/null +++ b/Sources/ProcessBarMonitor/LegacyLaunchAgentCleaner.swift @@ -0,0 +1,47 @@ +import Foundation + +struct LegacyLaunchAgentCleanupResult { + let removed: Bool + let messageKey: String? + let details: String? +} + +enum LegacyLaunchAgentCleaner { + static let legacyLabel = "ai.openclaw.ProcessBarMonitor" + + static func cleanupIfNeeded( + fileManager: FileManager = .default, + processRunner: ((String, [String]) throws -> Int32)? = nil + ) -> LegacyLaunchAgentCleanupResult { + let plistPath = (NSHomeDirectory() as NSString).appendingPathComponent("Library/LaunchAgents/\(legacyLabel).plist") + guard fileManager.fileExists(atPath: plistPath) else { + return LegacyLaunchAgentCleanupResult(removed: false, messageKey: nil, details: nil) + } + + let runner = processRunner ?? runProcess + do { + _ = try? runner("/bin/launchctl", ["bootout", "gui/\(getuid())", plistPath]) + try fileManager.removeItem(atPath: plistPath) + return LegacyLaunchAgentCleanupResult( + removed: true, + messageKey: "status.legacy_launch_agent_removed", + details: nil + ) + } catch { + return LegacyLaunchAgentCleanupResult( + removed: false, + messageKey: "status.legacy_launch_agent_remove_failed", + details: error.localizedDescription + ) + } + } + + private static func runProcess(_ launchPath: String, _ arguments: [String]) throws -> Int32 { + let process = Process() + process.executableURL = URL(fileURLWithPath: launchPath) + process.arguments = arguments + try process.run() + process.waitUntilExit() + return process.terminationStatus + } +} diff --git a/Sources/ProcessBarMonitor/MonitorViewModel.swift b/Sources/ProcessBarMonitor/MonitorViewModel.swift index b1828f4..136d07a 100644 --- a/Sources/ProcessBarMonitor/MonitorViewModel.swift +++ b/Sources/ProcessBarMonitor/MonitorViewModel.swift @@ -108,6 +108,17 @@ final class MonitorViewModel: ObservableObject { func start() { guard refreshTask == nil else { return } + + let legacyCleanup = LegacyLaunchAgentCleaner.cleanupIfNeeded() + if let messageKey = legacyCleanup.messageKey { + if let details = legacyCleanup.details { + statusMessage = L10n.format(messageKey, details) + } else { + statusMessage = L10n.string(messageKey) + } + } + launchAtLogin.refreshState() + refreshTask = Task { [weak self] in await self?.refresh(forceProcesses: true) while !Task.isCancelled { diff --git a/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings b/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings index da71afa..ff6c5b1 100644 --- a/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings +++ b/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings @@ -31,6 +31,8 @@ "status.failed_to_load_top_apps" = "Failed to load top apps: %@"; "status.launch_at_login_enabled" = "Launch at login enabled."; "status.launch_at_login_disabled" = "Launch at login disabled."; +"status.legacy_launch_agent_removed" = "Removed a leftover legacy login LaunchAgent so the app will no longer start twice."; +"status.legacy_launch_agent_remove_failed" = "Found a leftover legacy login LaunchAgent but could not remove it automatically: %@"; "thermal.nominal" = "Nominal"; "thermal.fair" = "Fair"; "thermal.serious" = "Serious"; diff --git a/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings b/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings index 9643c4b..3d8e65e 100644 --- a/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings @@ -31,6 +31,8 @@ "status.failed_to_load_top_apps" = "加载 Top Apps 失败:%@"; "status.launch_at_login_enabled" = "已启用开机启动。"; "status.launch_at_login_disabled" = "已关闭开机启动。"; +"status.legacy_launch_agent_removed" = "已清理遗留的旧版开机启动项,应用之后不会再因此重复启动。"; +"status.legacy_launch_agent_remove_failed" = "发现遗留的旧版开机启动项,但自动清理失败:%@"; "thermal.nominal" = "正常"; "thermal.fair" = "一般"; "thermal.serious" = "较高";