From 77ef2e61002668469b749ec715c4f0638b98f4d6 Mon Sep 17 00:00:00 2001 From: X1a0He Date: Sun, 20 Jul 2025 01:38:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=83=E4=BA=86=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E7=9A=84=E8=A5=BF=E7=93=9C=EF=BC=8C=E9=87=8D=E5=86=99Helper?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8macOS=2013.0+=E7=9A=84=20SMAppServic?= =?UTF-8?q?e=20=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Adobe Downloader/Adobe DownloaderApp.swift | 6 +- Adobe Downloader/AppDelegate.swift | 2 +- .../HelperMigrationManager.swift | 176 ++++ .../ModernPrivilegedHelperManager.swift | 605 ++++++++++++++ .../PrivilegedHelperAdapter.swift | 199 +++++ .../PrivilegedHelperManager.swift | 757 ------------------ Adobe Downloader/Info.plist | 4 +- Adobe Downloader/Utils/InstallManager.swift | 10 +- Adobe Downloader/Utils/ModifySetup.swift | 8 +- Adobe Downloader/Utils/NewDownloadUtils.swift | 2 +- Adobe Downloader/Views/AboutView.swift | 59 +- Adobe Downloader/Views/CleanConfigView.swift | 8 +- Adobe Downloader/Views/CleanupView.swift | 4 +- .../Views/DownloadProgressView.swift | 4 +- AdobeDownloaderHelperTool/Info.plist | 6 +- AdobeDownloaderHelperTool/Launchd.plist | 8 +- ...x1a0he.macOS.Adobe-Downloader.helper.plist | 23 + 17 files changed, 1080 insertions(+), 801 deletions(-) create mode 100644 Adobe Downloader/HelperManager/HelperMigrationManager.swift create mode 100644 Adobe Downloader/HelperManager/ModernPrivilegedHelperManager.swift create mode 100644 Adobe Downloader/HelperManager/PrivilegedHelperAdapter.swift delete mode 100644 Adobe Downloader/HelperManager/PrivilegedHelperManager.swift create mode 100644 AdobeDownloaderHelperTool/com.x1a0he.macOS.Adobe-Downloader.helper.plist diff --git a/Adobe Downloader/Adobe DownloaderApp.swift b/Adobe Downloader/Adobe DownloaderApp.swift index 69c4c49..bff92b4 100644 --- a/Adobe Downloader/Adobe DownloaderApp.swift +++ b/Adobe Downloader/Adobe DownloaderApp.swift @@ -129,8 +129,10 @@ struct Adobe_DownloaderApp: App { } private func setupApplication() async { - PrivilegedHelperManager.shared.checkInstall() - + Task { + await ModernPrivilegedHelperManager.shared.checkAndInstallHelper() + } + await MainActor.run { globalNetworkManager.loadSavedTasks() } diff --git a/Adobe Downloader/AppDelegate.swift b/Adobe Downloader/AppDelegate.swift index aa5595e..84d058b 100644 --- a/Adobe Downloader/AppDelegate.swift +++ b/Adobe Downloader/AppDelegate.swift @@ -34,7 +34,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { return event } - PrivilegedHelperManager.shared.executeCommand("id -u") { _ in } + ModernPrivilegedHelperManager.shared.executeCommand("id -u") { _ in } } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { diff --git a/Adobe Downloader/HelperManager/HelperMigrationManager.swift b/Adobe Downloader/HelperManager/HelperMigrationManager.swift new file mode 100644 index 0000000..215db0f --- /dev/null +++ b/Adobe Downloader/HelperManager/HelperMigrationManager.swift @@ -0,0 +1,176 @@ +// +// HelperMigrationManager.swift +// Adobe Downloader +// +// Created by X1a0He on 2025/07/20. +// + +import Foundation +import ServiceManagement +import os.log + +class HelperMigrationManager { + + private let logger = Logger(subsystem: "com.x1a0he.macOS.Adobe-Downloader", category: "Migration") + + static func performMigrationIfNeeded() async throws { + let migrationManager = HelperMigrationManager() + + if migrationManager.needsMigration() { + try await migrationManager.performMigration() + } + } + + private func needsMigration() -> Bool { + // 检查是否存在旧的 SMJobBless 安装 + let legacyPlistPath = "/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist" + let legacyHelperPath = "/Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper" + + let hasLegacyFiles = FileManager.default.fileExists(atPath: legacyPlistPath) || + FileManager.default.fileExists(atPath: legacyHelperPath) + + let appService = SMAppService.daemon(plistName: "com.x1a0he.macOS.Adobe-Downloader.helper") + let hasModernService = appService.status != .notRegistered + + logger.info("迁移检查 - 旧文件存在: \(hasLegacyFiles), 新服务已注册: \(hasModernService)") + + return hasLegacyFiles && !hasModernService + } + + private func performMigration() async throws { + logger.info("开始 Helper 迁移过程") + + try await stopLegacyService() + + try await cleanupLegacyFiles() + + try await registerModernService() + + try await validateNewService() + + logger.info("Helper 迁移完成") + } + + private func stopLegacyService() async throws { + logger.info("停止旧的 Helper 服务") + + let script = """ + #!/bin/bash + # 停止旧的 LaunchDaemon + sudo /bin/launchctl unload /Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist 2>/dev/null || true + + # 终止可能运行的进程 + sudo /usr/bin/killall -u root -9 com.x1a0he.macOS.Adobe-Downloader.helper 2>/dev/null || true + + exit 0 + """ + + try await executePrivilegedScript(script, description: "停止旧服务") + } + + private func cleanupLegacyFiles() async throws { + logger.info("清理旧的安装文件") + + let script = """ + #!/bin/bash + # 删除旧的 plist 文件 + sudo /bin/rm -f /Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist + + # 删除旧的 Helper 文件 + sudo /bin/rm -f /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper + + exit 0 + """ + + try await executePrivilegedScript(script, description: "清理旧文件") + } + + private func registerModernService() async throws { + logger.info("注册新的 SMAppService") + + let modernManager = ModernPrivilegedHelperManager.shared + await modernManager.checkAndInstallHelper() + + try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + } + + private func validateNewService() async throws { + logger.info("验证新服务") + + let modernManager = ModernPrivilegedHelperManager.shared + let status = await modernManager.getHelperStatus() + + switch status { + case .installed: + logger.info("新服务验证成功") + case .needsApproval: + logger.warning("新服务需要用户批准") + throw MigrationError.requiresUserApproval + default: + logger.error("新服务验证失败: \(String(describing: status))") + throw MigrationError.validationFailed + } + } + + private func executePrivilegedScript(_ script: String, description: String) async throws { + let tempDir = FileManager.default.temporaryDirectory + let scriptURL = tempDir.appendingPathComponent("migration_\(UUID().uuidString).sh") + + try script.write(to: scriptURL, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path) + + defer { + try? FileManager.default.removeItem(at: scriptURL) + } + + return try await withCheckedThrowingContinuation { continuation in + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript") + task.arguments = ["-e", "do shell script \"\(scriptURL.path)\" with administrator privileges"] + + do { + try task.run() + task.waitUntilExit() + + if task.terminationStatus == 0 { + self.logger.info("\(description) 执行成功") + continuation.resume() + } else { + self.logger.error("\(description) 执行失败: \(task.terminationStatus)") + continuation.resume(throwing: MigrationError.scriptExecutionFailed(description)) + } + } catch { + self.logger.error("\(description) 启动失败: \(error)") + continuation.resume(throwing: error) + } + } + } +} + +enum MigrationError: LocalizedError { + case requiresUserApproval + case validationFailed + case scriptExecutionFailed(String) + + var errorDescription: String? { + switch self { + case .requiresUserApproval: + return String(localized: "迁移完成,但需要在系统设置中批准新的 Helper 服务") + case .validationFailed: + return String(localized: "新 Helper 服务验证失败") + case .scriptExecutionFailed(let description): + return String(localized: "\(description) 执行失败") + } + } +} + +extension ModernPrivilegedHelperManager { + static func initializeWithMigration() async throws -> ModernPrivilegedHelperManager { + try await HelperMigrationManager.performMigrationIfNeeded() + + let manager = ModernPrivilegedHelperManager.shared + await manager.checkAndInstallHelper() + + return manager + } +} diff --git a/Adobe Downloader/HelperManager/ModernPrivilegedHelperManager.swift b/Adobe Downloader/HelperManager/ModernPrivilegedHelperManager.swift new file mode 100644 index 0000000..44c47bf --- /dev/null +++ b/Adobe Downloader/HelperManager/ModernPrivilegedHelperManager.swift @@ -0,0 +1,605 @@ +// +// ModernPrivilegedHelperManager.swift +// Adobe Downloader +// +// Created by X1a0He on 2025/07/20. +// + +import AppKit +import Cocoa +import ServiceManagement +import os.log + +@objc enum CommandType: Int { + case install + case uninstall + case moveFile + case setPermissions + case shellCommand +} + +@objc(HelperToolProtocol) protocol HelperToolProtocol { + @objc(executeCommand:path1:path2:permissions:withReply:) + func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void) + func getInstallationOutput(withReply reply: @escaping (String) -> Void) +} + +@objcMembers +class ModernPrivilegedHelperManager: NSObject, ObservableObject { + + enum HelperStatus { + case installed + case notInstalled + case needsApproval + case requiresUpdate + case legacy + } + + enum ConnectionState { + case connected + case disconnected + case connecting + + var description: String { + switch self { + case .connected: + return String(localized: "已连接") + case .disconnected: + return String(localized: "未连接") + case .connecting: + return String(localized: "正在连接") + } + } + } + + enum HelperError: LocalizedError { + case serviceUnavailable + case connectionFailed + case proxyError + case authorizationFailed + case installationFailed(String) + case legacyInstallationDetected + + var errorDescription: String? { + switch self { + case .serviceUnavailable: + return String(localized: "Helper 服务不可用") + case .connectionFailed: + return String(localized: "无法连接到 Helper") + case .proxyError: + return String(localized: "无法获取 Helper 代理") + case .authorizationFailed: + return String(localized: "获取授权失败") + case .installationFailed(let reason): + return String(localized: "安装失败: \(reason)") + case .legacyInstallationDetected: + return String(localized: "检测到旧版本安装,需要清理") + } + } + } + + static let shared = ModernPrivilegedHelperManager() + static let helperIdentifier = "com.x1a0he.macOS.Adobe-Downloader.helper" + + private let logger = Logger(subsystem: "com.x1a0he.macOS.Adobe-Downloader", category: "HelperManager") + + @Published private(set) var connectionState: ConnectionState = .disconnected + + private var appService: SMAppService? + private var connection: NSXPCConnection? + private let connectionQueue = DispatchQueue(label: "com.x1a0he.macOS.Adobe-Downloader.helper.connection") + + var connectionSuccessBlock: (() -> Void)? + private var shouldAutoReconnect = true + private var isInitializing = false + + override init() { + super.init() + initializeAppService() + setupAutoReconnect() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleConnectionInvalidation), + name: .NSXPCConnectionInvalid, + object: nil + ) + } + + private func initializeAppService() { + appService = SMAppService.daemon(plistName: "com.x1a0he.macOS.Adobe-Downloader.helper.plist") + } + + private func setupAutoReconnect() { + Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + if self.connectionState == .disconnected && self.shouldAutoReconnect { + Task { + await self.attemptConnection() + } + } + } + } + + func checkAndInstallHelper() async { + logger.info("开始检查 Helper 状态") + + let status = await getHelperStatus() + + await MainActor.run { + switch status { + case .legacy: + handleLegacyInstallation() + break + case .notInstalled: + registerHelper() + break + case .needsApproval: + showApprovalGuidance() + break + case .requiresUpdate: + updateHelper() + break + case .installed: + Task { + await attemptConnection() + connectionSuccessBlock?() + } + } + } + } + + func getHelperStatus() async -> HelperStatus { + guard let appService = appService else { + return .notInstalled + } + + if hasLegacyInstallation() { + return .legacy + } + + let status = appService.status + logger.info("SMAppService 状态: \(status.rawValue)") + + switch status { + case .notRegistered: + return .notInstalled + + case .enabled: + if await needsUpdate() { + return .requiresUpdate + } + return .installed + + case .requiresApproval: + return .needsApproval + + case .notFound: + return .notInstalled + + @unknown default: + logger.warning("未知的 SMAppService 状态: \(status.rawValue)") + return .notInstalled + } + } + + private func registerHelper() { + guard let appService = appService else { + logger.error("SMAppService 未初始化") + return + } + + do { + try appService.register() + logger.info("Helper 注册成功") + + if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { + UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild") + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + Task { + await self.attemptConnection() + } + } + + } catch { + logger.error("Helper 注册失败: \(error)") + handleRegistrationError(error) + } + } + + private func updateHelper() { + registerHelper() + } + + func uninstallHelper() async throws { + shouldAutoReconnect = false + await disconnectHelper() + + if let appService = appService { + do { + try await appService.unregister() + logger.info("SMAppService 卸载成功") + } catch { + logger.error("SMAppService 卸载失败: \(error)") + throw error + } + } + + try await cleanupLegacyInstallation() + + UserDefaults.standard.removeObject(forKey: "InstalledHelperBuild") + + await MainActor.run { + connectionState = .disconnected + } + } + + @discardableResult + private func attemptConnection() async -> Bool { + return connectionQueue.sync { + createConnection() != nil + } + } + + private func createConnection() -> NSXPCConnection? { + DispatchQueue.main.async { + self.connectionState = .connecting + } + + if let existingConnection = connection { + existingConnection.invalidate() + connection = nil + } + + let newConnection = NSXPCConnection(machServiceName: Self.helperIdentifier, options: .privileged) + + let interface = NSXPCInterface(with: HelperToolProtocol.self) + interface.setClasses( + NSSet(array: [NSString.self, NSNumber.self]) as! Set, + for: #selector(HelperToolProtocol.executeCommand(type:path1:path2:permissions:withReply:)), + argumentIndex: 1, + ofReply: false + ) + newConnection.remoteObjectInterface = interface + + newConnection.interruptionHandler = { [weak self] in + self?.logger.warning("XPC 连接中断") + DispatchQueue.main.async { + self?.connectionState = .disconnected + self?.connection = nil + } + } + + newConnection.invalidationHandler = { [weak self] in + self?.logger.info("XPC 连接失效") + DispatchQueue.main.async { + self?.connectionState = .disconnected + self?.connection = nil + } + } + + newConnection.resume() + + let semaphore = DispatchSemaphore(value: 0) + var isConnected = false + + if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol { + helper.executeCommand(type: .shellCommand, path1: "id -u", path2: "", permissions: 0) { [weak self] result in + if result.contains("0") || result == "0" { + isConnected = true + DispatchQueue.main.async { + self?.connection = newConnection + self?.connectionState = .connected + } + } + semaphore.signal() + } + _ = semaphore.wait(timeout: .now() + 1.0) + } + + if !isConnected { + newConnection.invalidate() + DispatchQueue.main.async { + self.connectionState = .disconnected + } + return nil + } + + logger.info("XPC 连接建立成功") + return newConnection + } + + func disconnectHelper() async { + connectionQueue.sync { + shouldAutoReconnect = false + connection?.invalidate() + connection = nil + + DispatchQueue.main.async { + self.connectionState = .disconnected + } + } + } + + func reconnectHelper() async throws { + await disconnectHelper() + shouldAutoReconnect = true + + if await attemptConnection() { + logger.info("重新连接成功") + } else { + throw HelperError.connectionFailed + } + } + + func getHelperProxy() throws -> HelperToolProtocol { + if connectionState != .connected { + guard let newConnection = connectionQueue.sync(execute: { createConnection() }) else { + throw HelperError.connectionFailed + } + connection = newConnection + } + + guard let helper = connection?.remoteObjectProxyWithErrorHandler({ [weak self] error in + self?.logger.error("XPC 代理错误: \(error)") + self?.connectionState = .disconnected + }) as? HelperToolProtocol else { + throw HelperError.proxyError + } + + return helper + } + + func executeCommand(_ command: String, completion: @escaping (String) -> Void) { + do { + let helper = try getHelperProxy() + + if command.contains("perl") || command.contains("codesign") || command.contains("xattr") { + helper.executeCommand(type: .shellCommand, path1: command, path2: "", permissions: 0) { [weak self] result in + DispatchQueue.main.async { + self?.updateConnectionState(from: result) + completion(result) + } + } + return + } + + let (type, path1, path2, permissions) = parseCommand(command) + + helper.executeCommand(type: type, path1: path1, path2: path2, permissions: permissions) { [weak self] result in + DispatchQueue.main.async { + self?.updateConnectionState(from: result) + completion(result) + } + } + } catch { + connectionState = .disconnected + completion("Error: \(error.localizedDescription)") + } + } + + func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws { + let helper: HelperToolProtocol = try connectionQueue.sync { + if let existingConnection = connection, + let proxy = existingConnection.remoteObjectProxy as? HelperToolProtocol { + return proxy + } + + guard let newConnection = createConnection() else { + throw HelperError.connectionFailed + } + + connection = newConnection + + guard let proxy = newConnection.remoteObjectProxy as? HelperToolProtocol else { + throw HelperError.proxyError + } + + return proxy + } + + let (type, path1, path2, permissions) = parseCommand(command) + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + helper.executeCommand(type: type, path1: path1, path2: path2, permissions: permissions) { result in + if result == "Started" || result == "Success" { + continuation.resume() + } else { + continuation.resume(throwing: HelperError.installationFailed(result)) + } + } + } + + while true { + let output = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + helper.getInstallationOutput { result in + continuation.resume(returning: result) + } + } + + if !output.isEmpty { + progress(output) + } + + if output.contains("Exit Code:") || output.range(of: "Progress: \\d+/\\d+", options: .regularExpression) != nil { + if output.range(of: "Progress: \\d+/\\d+", options: .regularExpression) != nil { + progress("Exit Code: 0") + } + break + } + + try await Task.sleep(nanoseconds: 100_000_000) + } + } + + private func updateConnectionState(from result: String) { + if result.starts(with: "Error:") { + connectionState = .disconnected + } else { + connectionState = .connected + } + } + + private func parseCommand(_ command: String) -> (CommandType, String, String, Int) { + let components = command.split(separator: " ", omittingEmptySubsequences: true).map(String.init) + + if command.hasPrefix("installer -pkg") { + return (.install, components[2], "", 0) + } else if command.hasPrefix("rm -rf") { + let path = components.dropFirst(2).joined(separator: " ") + return (.uninstall, path, "", 0) + } else if command.hasPrefix("mv") || command.hasPrefix("cp") { + let paths = components.dropFirst(1) + let sourcePath = String(paths.first ?? "") + let destPath = paths.dropFirst().joined(separator: " ") + return (.moveFile, sourcePath, destPath, 0) + } else if command.hasPrefix("chmod") { + return (.setPermissions, + components.dropFirst(2).joined(separator: " "), + "", + Int(components[1]) ?? 0) + } + + return (.shellCommand, command, "", 0) + } + + private func needsUpdate() async -> Bool { + guard let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, + let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") else { + return true + } + + return currentBuild != installedBuild + } + + @objc private func handleConnectionInvalidation() { + DispatchQueue.main.async { [weak self] in + self?.connectionState = .disconnected + self?.connection?.invalidate() + self?.connection = nil + } + } + + private func hasLegacyInstallation() -> Bool { + let legacyPlistPath = "/Library/LaunchDaemons/\(Self.helperIdentifier).plist" + let legacyHelperPath = "/Library/PrivilegedHelperTools/\(Self.helperIdentifier)" + + return FileManager.default.fileExists(atPath: legacyPlistPath) || + FileManager.default.fileExists(atPath: legacyHelperPath) + } + + private func handleLegacyInstallation() { + logger.info("检测到旧的 SMJobBless 安装,开始清理") + + let alert = NSAlert() + alert.messageText = String(localized: "检测到旧版本的 Helper") + alert.informativeText = String(localized: "系统检测到旧版本的 Adobe Downloader Helper,需要升级到新版本。这将需要管理员权限来清理旧安装。") + alert.addButton(withTitle: String(localized: "升级")) + alert.addButton(withTitle: String(localized: "取消")) + + if alert.runModal() == .alertFirstButtonReturn { + Task { + do { + try await cleanupLegacyInstallation() + registerHelper() + } catch { + logger.error("清理旧安装失败: \(error)") + showError("清理旧版本失败: \(error.localizedDescription)") + } + } + } + } + + private func cleanupLegacyInstallation() async throws { + let script = """ + #!/bin/bash + sudo /bin/launchctl unload /Library/LaunchDaemons/\(Self.helperIdentifier).plist 2>/dev/null + sudo /bin/rm -f /Library/LaunchDaemons/\(Self.helperIdentifier).plist + sudo /bin/rm -f /Library/PrivilegedHelperTools/\(Self.helperIdentifier) + sudo /usr/bin/killall -u root -9 \(Self.helperIdentifier) 2>/dev/null || true + exit 0 + """ + + let tempDir = FileManager.default.temporaryDirectory + let scriptURL = tempDir.appendingPathComponent("cleanup_legacy_helper.sh") + + try script.write(to: scriptURL, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path) + + return try await withCheckedThrowingContinuation { continuation in + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript") + task.arguments = ["-e", "do shell script \"\(scriptURL.path)\" with administrator privileges"] + + do { + try task.run() + task.waitUntilExit() + + try? FileManager.default.removeItem(at: scriptURL) + + if task.terminationStatus == 0 { + logger.info("旧安装清理成功") + continuation.resume() + } else { + continuation.resume(throwing: HelperError.installationFailed("清理脚本执行失败")) + } + } catch { + try? FileManager.default.removeItem(at: scriptURL) + continuation.resume(throwing: error) + } + } + } + + private func showApprovalGuidance() { + let alert = NSAlert() + alert.messageText = String(localized: "需要在系统设置中允许 Helper") + alert.informativeText = String(localized: "Adobe Downloader 需要通过后台服务来安装与移动文件。请在\"系统设置 → 通用 → 登录项与扩展\"中允许此应用的后台项目。") + alert.addButton(withTitle: String(localized: "打开系统设置")) + alert.addButton(withTitle: String(localized: "稍后设置")) + + if alert.runModal() == .alertFirstButtonReturn { + SMAppService.openSystemSettingsLoginItems() + } + } + + private func handleRegistrationError(_ error: Error) { + logger.error("Helper 注册错误: \(error)") + + let nsError = error as NSError + + let message = String(localized: "Helper 注册失败") + var informative = error.localizedDescription + + if nsError.domain == "com.apple.ServiceManagement.SMAppServiceError" { + switch nsError.code { + case 1: // kSMAppServiceErrorDomain + informative = String(localized: "Helper 文件不存在或损坏,请重新安装应用") + case 2: // Permission denied + informative = String(localized: "权限被拒绝,请检查应用签名和权限设置") + default: + break + } + } + + showError(message, informative: informative) + } + + private func showError(_ message: String, informative: String? = nil) { + DispatchQueue.main.async { + let alert = NSAlert() + alert.messageText = message + if let informative = informative { + alert.informativeText = informative + } + alert.alertStyle = .warning + alert.addButton(withTitle: String(localized: "确定")) + alert.runModal() + } + } +} + +fileprivate extension Notification.Name { + static let NSXPCConnectionInvalid = Notification.Name("NSXPCConnectionInvalidNotification") +} diff --git a/Adobe Downloader/HelperManager/PrivilegedHelperAdapter.swift b/Adobe Downloader/HelperManager/PrivilegedHelperAdapter.swift new file mode 100644 index 0000000..836d004 --- /dev/null +++ b/Adobe Downloader/HelperManager/PrivilegedHelperAdapter.swift @@ -0,0 +1,199 @@ +// +// PrivilegedHelperAdapter.swift +// Adobe Downloader +// +// Created by X1a0He on 2025/07/20. +// + +import Foundation +import AppKit +import ServiceManagement + +@objcMembers +class PrivilegedHelperAdapter: NSObject, ObservableObject { + + static let shared = PrivilegedHelperAdapter() + static let machServiceName = "com.x1a0he.macOS.Adobe-Downloader.helper" + + @Published var connectionState: ConnectionState = .disconnected + + private let modernManager: ModernPrivilegedHelperManager + var connectionSuccessBlock: (() -> Void)? + + enum HelperStatus { + case installed + case noFound + case needUpdate + } + + enum ConnectionState { + case connected + case disconnected + case connecting + + var description: String { + switch self { + case .connected: + return String(localized: "已连接") + case .disconnected: + return String(localized: "未连接") + case .connecting: + return String(localized: "正在连接") + } + } + } + + override init() { + self.modernManager = ModernPrivilegedHelperManager.shared + super.init() + + modernManager.$connectionState + .receive(on: DispatchQueue.main) + .sink { [weak self] modernState in + self?.connectionState = self?.convertConnectionState(modernState) ?? .disconnected + } + .store(in: &cancellables) + + Task { + await initializeWithMigration() + } + } + + private var cancellables = Set() + + func checkInstall() { + Task { + await modernManager.checkAndInstallHelper() + } + } + + func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) { + Task { + let modernStatus = await modernManager.getHelperStatus() + let legacyStatus = convertHelperStatus(modernStatus) + + await MainActor.run { + callback(legacyStatus) + } + } + } + + static var getHelperStatus: Bool { + let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + machServiceName) + guard CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else { return false } + + let appService = SMAppService.daemon(plistName: machServiceName) + return appService.status == .enabled + } + + func executeCommand(_ command: String, completion: @escaping (String) -> Void) { + modernManager.executeCommand(command, completion: completion) + } + + func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws { + try await modernManager.executeInstallation(command, progress: progress) + } + + func reconnectHelper(completion: @escaping (Bool, String) -> Void) { + Task { + do { + try await modernManager.reconnectHelper() + completion(true, String(localized: "重新连接成功")) + } catch { + completion(false, error.localizedDescription) + } + } + } + + func reinstallHelper(completion: @escaping (Bool, String) -> Void) { + Task { + do { + try await modernManager.uninstallHelper() + await modernManager.checkAndInstallHelper() + + try await Task.sleep(nanoseconds: 2_000_000_000) + + let status = await modernManager.getHelperStatus() + switch status { + case .installed: + completion(true, String(localized: "重新安装成功")) + case .needsApproval: + completion(false, String(localized: "需要在系统设置中批准")) + default: + completion(false, String(localized: "重新安装失败")) + } + } catch { + completion(false, error.localizedDescription) + } + } + } + + func removeInstallHelper(completion: ((Bool) -> Void)? = nil) { + Task { + do { + try await modernManager.uninstallHelper() + completion?(true) + } catch { + completion?(false) + } + } + } + + func forceReinstallHelper() { + reinstallHelper { _, _ in } + } + + func disconnectHelper() { + Task { + await modernManager.disconnectHelper() + } + } + + func uninstallHelperViaTerminal(completion: @escaping (Bool, String) -> Void) { + Task { + do { + try await modernManager.uninstallHelper() + completion(true, String(localized: "Helper 已完全卸载")) + } catch { + completion(false, error.localizedDescription) + } + } + } + + public func getHelperProxy() throws -> HelperToolProtocol { + return try modernManager.getHelperProxy() + } + + private func initializeWithMigration() async { + do { + let _ = try await ModernPrivilegedHelperManager.initializeWithMigration() + connectionSuccessBlock?() + } catch { + print("Helper 初始化失败: \(error)") + } + } + + private func convertConnectionState(_ modernState: ModernPrivilegedHelperManager.ConnectionState) -> ConnectionState { + switch modernState { + case .connected: + return .connected + case .disconnected: + return .disconnected + case .connecting: + return .connecting + } + } + + private func convertHelperStatus(_ modernStatus: ModernPrivilegedHelperManager.HelperStatus) -> HelperStatus { + switch modernStatus { + case .installed: + return .installed + case .notInstalled, .needsApproval, .legacy: + return .noFound + case .requiresUpdate: + return .needUpdate + } + } +} + +import Combine diff --git a/Adobe Downloader/HelperManager/PrivilegedHelperManager.swift b/Adobe Downloader/HelperManager/PrivilegedHelperManager.swift deleted file mode 100644 index 6ce62d3..0000000 --- a/Adobe Downloader/HelperManager/PrivilegedHelperManager.swift +++ /dev/null @@ -1,757 +0,0 @@ -// -// Untitled.swift -// Adobe Downloader -// -// Created by X1a0He on 11/12/24. -// - -import AppKit -import Cocoa -import ServiceManagement - -@objc enum CommandType: Int { - case install - case uninstall - case moveFile - case setPermissions - case shellCommand -} - -@objc protocol HelperToolProtocol { - @objc(executeCommand:path1:path2:permissions:withReply:) - func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void) - func getInstallationOutput(withReply reply: @escaping (String) -> Void) -} - -@objcMembers -class PrivilegedHelperManager: NSObject { - - enum HelperStatus { - case installed - case noFound - case needUpdate - } - - static let shared = PrivilegedHelperManager() - static let machServiceName = "com.x1a0he.macOS.Adobe-Downloader.helper" - var connectionSuccessBlock: (() -> Void)? - - private var useLegacyInstall = false - private var connection: NSXPCConnection? - private var isInitializing = false - private var shouldAutoReconnect = true - - @Published private(set) var connectionState: ConnectionState = .disconnected { - didSet { - if oldValue != connectionState { - if connectionState == .disconnected { - connection?.invalidate() - connection = nil - } - } - } - } - - enum ConnectionState { - case connected - case disconnected - case connecting - - var description: String { - switch self { - case .connected: - return String(localized: "已连接") - case .disconnected: - return String(localized: "未连接") - case .connecting: - return String(localized: "正在连接") - } - } - } - - private let connectionQueue = DispatchQueue(label: "com.x1a0he.helper.connection") - - override init() { - super.init() - initAuthorizationRef() - setupAutoReconnect() - - NotificationCenter.default.addObserver(self, - selector: #selector(handleConnectionInvalidation), - name: .NSXPCConnectionInvalid, - object: nil) - } - - @objc private func handleConnectionInvalidation() { - DispatchQueue.main.async { [weak self] in - self?.connectionState = .disconnected - self?.connection?.invalidate() - self?.connection = nil - } - } - - private func setupAutoReconnect() { - Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in - guard let self = self else { return } - if self.connectionState == .disconnected && self.shouldAutoReconnect { - _ = self.connectToHelper() - } - } - } - - func checkInstall() { - if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, - let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") { - if currentBuild != installedBuild { - notifyInstall() - return - } - } - - getHelperStatus { [weak self] status in - guard let self = self else { return } - switch status { - case .noFound: - if #available(macOS 13, *) { - let url = URL(string: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist")! - let status = SMAppService.statusForLegacyPlist(at: url) - if status == .requiresApproval { - let alert = NSAlert() - let notice = String(localized: "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件,请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App") - let addition = String(localized: "如果在设置里没找到当前App,可以尝试重置守护程序") - alert.messageText = notice + "\n" + addition - alert.addButton(withTitle: "打开系统登录项设置") - alert.addButton(withTitle: "重置守护程序") - if alert.runModal() == .alertFirstButtonReturn { - SMAppService.openSystemSettingsLoginItems() - } else { - removeInstallHelper() - } - } - } - fallthrough - case .needUpdate: - if Thread.isMainThread { - self.notifyInstall() - } else { - DispatchQueue.main.async { - self.notifyInstall() - } - } - case .installed: - self.connectionSuccessBlock?() - } - } - } - - private func initAuthorizationRef() { - var authRef: AuthorizationRef? - let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authRef) - if status != OSStatus(errAuthorizationSuccess) { - return - } - } - - private func installHelperDaemon() -> DaemonInstallResult { - var authRef: AuthorizationRef? - var authStatus = AuthorizationCreate(nil, nil, [], &authRef) - - guard authStatus == errAuthorizationSuccess else { - return .authorizationFail - } - - var authItem = AuthorizationItem(name: (kSMRightBlessPrivilegedHelper as NSString).utf8String!, valueLength: 0, value: nil, flags: 0) - var authRights = withUnsafeMutablePointer(to: &authItem) { pointer in - AuthorizationRights(count: 1, items: pointer) - } - let flags: AuthorizationFlags = [[], .interactionAllowed, .extendRights, .preAuthorize] - authStatus = AuthorizationCreate(&authRights, nil, flags, &authRef) - defer { - if let ref = authRef { - AuthorizationFree(ref, []) - } - } - guard authStatus == errAuthorizationSuccess else { - return .getAdminFail - } - - var error: Unmanaged? - - if SMJobBless(kSMDomainSystemLaunchd, PrivilegedHelperManager.machServiceName as CFString, authRef, &error) == false { - if let blessError = error?.takeRetainedValue() { - let nsError = blessError as Error as NSError - NSAlert.alert(with: "SMJobBless failed with error: \(blessError)\nError domain: \(nsError.domain)\nError code: \(nsError.code)\nError description: \(nsError.localizedDescription)\nError user info: \(nsError.userInfo)") - return .blessError(nsError.code) - } - return .blessError(-1) - } - - if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { - UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild") - } - return .success - } - - func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) { - var called = false - let reply: ((HelperStatus) -> Void) = { - status in - if called { return } - called = true - callback(status) - } - - let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName) - guard - CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else { - reply(.noFound) - return - } - - let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") - if !helperFileExists { - reply(.noFound) - return - } - - if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, - let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild"), - currentBuild != installedBuild { - reply(.needUpdate) - return - } - - reply(.installed) - } - - static var getHelperStatus: Bool { - if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String, - let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild"), - currentBuild != installedBuild { - return false - } - - let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + machServiceName) - guard CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else { return false } - return FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(machServiceName)") - } - - func reinstallHelper(completion: @escaping (Bool, String) -> Void) { - shouldAutoReconnect = false - uninstallHelperViaTerminal { [weak self] success, message in - guard let self = self else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - let result = self.installHelperDaemon() - - switch result { - case .success: - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - guard let self = self else { return } - self.shouldAutoReconnect = true - self.tryConnect(retryCount: 3, delay: 1, completion: completion) - } - - case .authorizationFail: - self.shouldAutoReconnect = true - completion(false, String(localized: "获取授权失败")) - case .getAdminFail: - self.shouldAutoReconnect = true - completion(false, String(localized: "获取管理员权限失败")) - case .blessError(_): - self.shouldAutoReconnect = true - completion(false, String(localized: "安装失败: \(result.alertContent)")) - } - } - } - } - - private func tryConnect(retryCount: Int, delay: TimeInterval = 2.0, completion: @escaping (Bool, String) -> Void) { - struct Static { - static var currentAttempt = 0 - } - - if retryCount == 3 { - Static.currentAttempt = 0 - } - - Static.currentAttempt += 1 - - guard retryCount > 0 else { - completion(false, String(localized: "多次尝试连接失败")) - return - } - - guard let connection = connectToHelper() else { - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in - self?.tryConnect(retryCount: retryCount - 1, delay: delay * 1, completion: completion) - } - return - } - - guard let helper = connection.remoteObjectProxy as? HelperToolProtocol else { - completion(false, String(localized: "无法获取Helper代理")) - return - } - - helper.executeCommand(type: .shellCommand, path1: "id -u", path2: "", permissions: 0) { result in - if result == "0" || result.contains("0") { - completion(true, String(localized: "Helper 重新安装成功")) - } else { - print("Helper验证失败,返回结果: \(result)") - completion(false, String(localized: "Helper 安装失败: \(result)")) - } - } - } - - func removeInstallHelper(completion: ((Bool) -> Void)? = nil) { - if FileManager.default.fileExists(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist") { - try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist") - } - if FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") { - try? FileManager.default.removeItem(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") - } - completion?(true) - } - - func connectToHelper() -> NSXPCConnection? { - return connectionQueue.sync { - return createConnection() - } - } - - private func createConnection() -> NSXPCConnection? { - DispatchQueue.main.async { - self.connectionState = .connecting - } - - if let existingConnection = connection { - existingConnection.invalidate() - connection = nil - } - - let newConnection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName, - options: .privileged) - - let interface = NSXPCInterface(with: HelperToolProtocol.self) - interface.setClasses(NSSet(array: [NSString.self, NSNumber.self]) as! Set, - for: #selector(HelperToolProtocol.executeCommand(type:path1:path2:permissions:withReply:)), - argumentIndex: 1, - ofReply: false) - newConnection.remoteObjectInterface = interface - - newConnection.interruptionHandler = { [weak self] in - DispatchQueue.main.async { - self?.connectionState = .disconnected - self?.connection = nil - } - } - - newConnection.invalidationHandler = { [weak self] in - DispatchQueue.main.async { - self?.connectionState = .disconnected - self?.connection = nil - } - } - - newConnection.resume() - - let semaphore = DispatchSemaphore(value: 0) - var isConnected = false - - if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol { - helper.executeCommand(type: .shellCommand, path1: "id -u", path2: "", permissions: 0) { [weak self] result in - if result.contains("0") || result == "0" { - isConnected = true - DispatchQueue.main.async { - self?.connection = newConnection - self?.connectionState = .connected - } - } - semaphore.signal() - } - _ = semaphore.wait(timeout: .now() + 1.0) - } - - if !isConnected { - newConnection.invalidate() - DispatchQueue.main.async { - self.connectionState = .disconnected - } - return nil - } - - return newConnection - } - - func executeCommand(_ command: String, completion: @escaping (String) -> Void) { - do { - let helper = try getHelperProxy() - - if command.contains("perl") || command.contains("codesign") || command.contains("xattr") { - helper.executeCommand(type: .shellCommand, path1: command, path2: "", permissions: 0) { [weak self] result in - DispatchQueue.main.async { - if result.starts(with: "Error:") { - self?.connectionState = .disconnected - } else { - self?.connectionState = .connected - } - completion(result) - } - } - return - } - - let (type, path1, path2, permissions) = parseCommand(command) - - helper.executeCommand(type: type, path1: path1, path2: path2, permissions: permissions) { [weak self] result in - DispatchQueue.main.async { - if result.starts(with: "Error:") { - self?.connectionState = .disconnected - } else { - self?.connectionState = .connected - } - completion(result) - } - } - } catch { - connectionState = .disconnected - completion("Error: \(error.localizedDescription)") - } - } - - private func parseCommand(_ command: String) -> (CommandType, String, String, Int) { - let components = command.split(separator: " ", omittingEmptySubsequences: true).map(String.init) - - if command.hasPrefix("installer -pkg") { - return (.install, components[2], "", 0) - } else if command.hasPrefix("rm -rf") { - let path = components.dropFirst(2).joined(separator: " ") - return (.uninstall, path, "", 0) - } else if command.hasPrefix("mv") || command.hasPrefix("cp") { - let paths = components.dropFirst(1) - let sourcePath = String(paths.first ?? "") - let destPath = paths.dropFirst().joined(separator: " ") - return (.moveFile, sourcePath, destPath, 0) - } else if command.hasPrefix("chmod") { - return (.setPermissions, - components.dropFirst(2).joined(separator: " "), - "", - Int(components[1]) ?? 0) - } - - return (.shellCommand, command, "", 0) - } - - func reconnectHelper(completion: @escaping (Bool, String) -> Void) { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.connectionState = .disconnected - self.connection?.invalidate() - self.connection = nil - self.shouldAutoReconnect = true - - DispatchQueue.main.asyncAfter(deadline: .now()) { - do { - let helper = try self.getHelperProxy() - - helper.executeCommand(type: .install, path1: "id -u", path2: "", permissions: 0) { result in - DispatchQueue.main.async { - if result.contains("0") || result == "0" { - self.connectionState = .connected - completion(true, String(localized: "Helper 重新连接成功")) - } else { - self.connectionState = .disconnected - completion(false, String(localized: "Helper 响应异常: \(result)")) - } - } - } - } catch HelperError.connectionFailed { - completion(false, String(localized: "无法连接到 Helper")) - } catch HelperError.proxyError { - completion(false, String(localized: "无法获取 Helper 代理")) - } catch { - completion(false, String(localized: "连接出现错误: \(error.localizedDescription)")) - } - } - } - } - - func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws { - let helper: HelperToolProtocol = try connectionQueue.sync { - if let existingConnection = connection, - let proxy = existingConnection.remoteObjectProxy as? HelperToolProtocol { - return proxy - } - - guard let newConnection = createConnection() else { - throw HelperError.connectionFailed - } - - connection = newConnection - - guard let proxy = newConnection.remoteObjectProxy as? HelperToolProtocol else { - throw HelperError.proxyError - } - - return proxy - } - - let (type, path1, path2, permissions) = parseCommand(command) - - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - helper.executeCommand(type: type, path1: path1, path2: path2, permissions: permissions) { result in - if result == "Started" || result == "Success" { - continuation.resume() - } else { - continuation.resume(throwing: HelperError.installationFailed(result)) - } - } - } - - while true { - let output = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - helper.getInstallationOutput { result in - continuation.resume(returning: result) - } - } - - if !output.isEmpty { - progress(output) - } - - if output.contains("Exit Code:") || output.range(of: "Progress: \\d+/\\d+", options: .regularExpression) != nil { - if output.range(of: "Progress: \\d+/\\d+", options: .regularExpression) != nil { - progress("Exit Code: 0") - } - break - } - - try await Task.sleep(nanoseconds: 100_000_000) - } - } - - func forceReinstallHelper() { - guard !isInitializing else { return } - isInitializing = true - shouldAutoReconnect = false - - uninstallHelperViaTerminal { [weak self] success, _ in - guard let self = self else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.notifyInstall() - self.isInitializing = false - self.shouldAutoReconnect = true - } - } - } - - func disconnectHelper() { - connectionQueue.sync { - shouldAutoReconnect = false - if let existingConnection = connection { - existingConnection.invalidate() - } - connection = nil - connectionState = .disconnected - } - } - func uninstallHelperViaTerminal(completion: @escaping (Bool, String) -> Void) { - disconnectHelper() - let script = """ - #!/bin/bash - sudo /bin/launchctl unload /Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist - sudo /bin/rm -f /Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist - sudo /bin/rm -f /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) - sudo /usr/bin/killall -u root -9 \(PrivilegedHelperManager.machServiceName) - exit 0 - """ - - let tempDir = FileManager.default.temporaryDirectory - let scriptURL = tempDir.appendingPathComponent("uninstall_helper.sh") - - do { - try script.write(to: scriptURL, atomically: true, encoding: .utf8) - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path) - - let task = Process() - task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript") - task.arguments = ["-e", "do shell script \"\(scriptURL.path)\" with administrator privileges"] - - let outputPipe = Pipe() - let errorPipe = Pipe() - task.standardOutput = outputPipe - task.standardError = errorPipe - - do { - try task.run() - task.waitUntilExit() - - if task.terminationStatus == 0 { - UserDefaults.standard.removeObject(forKey: "InstalledHelperBuild") - - connectionState = .disconnected - connection = nil - - completion(true, String(localized: "Helper 已完全卸载")) - } else { - let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() - let errorString = String(data: errorData, encoding: .utf8) ?? "未知错误" - completion(false, String(localized: "卸载失败: \(errorString)")) - } - - self.shouldAutoReconnect = true - - } catch { - self.shouldAutoReconnect = true - completion(false, String(localized: "执行卸载脚本失败: \(error.localizedDescription)")) - } - - try? FileManager.default.removeItem(at: scriptURL) - - } catch { - self.shouldAutoReconnect = true - completion(false, String(localized: "准备卸载脚本失败: \(error.localizedDescription)")) - } - } -} - -extension PrivilegedHelperManager { - private func notifyInstall() { - guard !isInitializing else { return } - shouldAutoReconnect = false - - let result = installHelperDaemon() - if case .success = result { - shouldAutoReconnect = true - checkInstall() - return - } - result.alertAction() - let ret = result.shouldRetryLegacyWay() - useLegacyInstall = ret.0 - let isCancle = ret.1 - if !isCancle, useLegacyInstall { - shouldAutoReconnect = true - checkInstall() - } else if isCancle, !useLegacyInstall { - shouldAutoReconnect = true - NSAlert.alert(with: String(localized: "获取管理员授权失败,用户主动取消授权!")) - } else { - shouldAutoReconnect = true - } - } -} - -private enum DaemonInstallResult { - case success - case authorizationFail - case getAdminFail - case blessError(Int) - var alertContent: String { - switch self { - case .success: - return "" - case .authorizationFail: return "Failed to create authorization!" - case .getAdminFail: return "The user actively cancels the authorization, Failed to get admin authorization! " - case let .blessError(code): - switch code { - case kSMErrorInternalFailure: return "blessError: kSMErrorInternalFailure" - case kSMErrorInvalidSignature: return "blessError: kSMErrorInvalidSignature" - case kSMErrorAuthorizationFailure: return "blessError: kSMErrorAuthorizationFailure" - case kSMErrorToolNotValid: return "blessError: kSMErrorToolNotValid" - case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound" - case kSMErrorServiceUnavailable: return "blessError: kSMErrorServiceUnavailable" - case kSMErrorJobMustBeEnabled: return "Adobe Downloader Helper is disabled by other process. Please run \"sudo launchctl enable system/\(PrivilegedHelperManager.machServiceName)\" in your terminal. The command has been copied to your pasteboard" - case kSMErrorInvalidPlist: return "blessError: kSMErrorInvalidPlist" - default: - return "bless unknown error:\(code)" - } - } - } - - func shouldRetryLegacyWay() -> (Bool, Bool) { - switch self { - case .success: return (false, false) - case let .blessError(code): - switch code { - case kSMErrorJobMustBeEnabled: - return (false, false) - default: - return (true, false) - } - case .authorizationFail: - return (true, false) - case .getAdminFail: - return (false, true) - } - } - - func alertAction() { - switch self { - case let .blessError(code): - switch code { - case kSMErrorJobMustBeEnabled: - NSPasteboard.general.clearContents() - NSPasteboard.general.setString("sudo launchctl enable system/\(PrivilegedHelperManager.machServiceName)", forType: .string) - default: - break - } - default: - break - } - } -} - -extension NSAlert { - static func alert(with text: String) { - let alert = NSAlert() - alert.messageText = text - alert.alertStyle = .warning - alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) - alert.runModal() - } -} - -enum HelperError: LocalizedError { - case connectionFailed - case proxyError - case authorizationFailed - case installationFailed(String) - - var errorDescription: String? { - switch self { - case .connectionFailed: - return String(localized: "无法连接到 Helper") - case .proxyError: - return String(localized: "无法获取 Helper 代理") - case .authorizationFailed: - return String(localized: "获取授权失败") - case .installationFailed(let reason): - return String(localized: "安装失败: \(reason)") - } - } -} - -extension PrivilegedHelperManager { - public func getHelperProxy() throws -> HelperToolProtocol { - if connectionState != .connected { - guard let newConnection = connectToHelper() else { - throw HelperError.connectionFailed - } - connection = newConnection - } - - guard let helper = connection?.remoteObjectProxyWithErrorHandler({ [weak self] error in - self?.connectionState = .disconnected - }) as? HelperToolProtocol else { - throw HelperError.proxyError - } - - return helper - } -} - -extension Notification.Name { - static let NSXPCConnectionInvalid = Notification.Name("NSXPCConnectionInvalidNotification") -} diff --git a/Adobe Downloader/Info.plist b/Adobe Downloader/Info.plist index 9891aff..b40f88e 100644 --- a/Adobe Downloader/Info.plist +++ b/Adobe Downloader/Info.plist @@ -11,8 +11,8 @@ SMPrivilegedExecutables - com.x1a0he.macOS.Adobe-Downloader.helper - identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ + com.x1a0he.macOS.Adobe-Downloader.helper + identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ SUFeedURL https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml diff --git a/Adobe Downloader/Utils/InstallManager.swift b/Adobe Downloader/Utils/InstallManager.swift index 4d8205e..70de67a 100644 --- a/Adobe Downloader/Utils/InstallManager.swift +++ b/Adobe Downloader/Utils/InstallManager.swift @@ -37,7 +37,7 @@ actor InstallManager { private func terminateSetupProcesses() async { let _ = await withCheckedContinuation { continuation in - PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { result in + ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { result in continuation.resume(returning: result) } } @@ -155,7 +155,7 @@ actor InstallManager { for logFile in logFiles { let removeCommand = "rm -f '\(logFile)'" let result = await withCheckedContinuation { continuation in - PrivilegedHelperManager.shared.executeCommand(removeCommand) { result in + ModernPrivilegedHelperManager.shared.executeCommand(removeCommand) { result in continuation.resume(returning: result) } } @@ -174,7 +174,7 @@ actor InstallManager { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in Task.detached { do { - try await PrivilegedHelperManager.shared.executeInstallation(installCommand) { output in + try await ModernPrivilegedHelperManager.shared.executeInstallation(installCommand) { output in Task { @MainActor in if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression), let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces), @@ -182,7 +182,7 @@ actor InstallManager { if exitCode == 0 { progressHandler(1.0, String(localized: "安装完成")) - PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } + ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } continuation.resume() return } else { @@ -241,7 +241,7 @@ actor InstallManager { } func cancel() { - PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } + ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } } func getInstallCommand(for driverPath: String) -> String { diff --git a/Adobe Downloader/Utils/ModifySetup.swift b/Adobe Downloader/Utils/ModifySetup.swift index fb826b3..9f19f52 100644 --- a/Adobe Downloader/Utils/ModifySetup.swift +++ b/Adobe Downloader/Utils/ModifySetup.swift @@ -84,14 +84,14 @@ class ModifySetup { if isSetupBackup() { print("检测到备份文件,尝试从备份恢复...") - PrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(backupPath)' '\(setupPath)'") { result in + ModernPrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(backupPath)' '\(setupPath)'") { result in if result.starts(with: "Error:") { print("从备份恢复失败: \(result)") } completion(!result.starts(with: "Error:")) } } else { - PrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(setupPath)' '\(backupPath)'") { result in + ModernPrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(setupPath)' '\(backupPath)'") { result in if result.starts(with: "Error:") { print("创建备份失败: \(result)") completion(false) @@ -100,7 +100,7 @@ class ModifySetup { if !result.starts(with: "Error:") { if FileManager.default.fileExists(atPath: backupPath) { - PrivilegedHelperManager.shared.executeCommand("/bin/chmod 644 '\(backupPath)'") { chmodResult in + ModernPrivilegedHelperManager.shared.executeCommand("/bin/chmod 644 '\(backupPath)'") { chmodResult in if chmodResult.starts(with: "Error:") { print("设置备份文件权限失败: \(chmodResult)") } @@ -138,7 +138,7 @@ class ModifySetup { return } - PrivilegedHelperManager.shared.executeCommand(commands[index]) { result in + ModernPrivilegedHelperManager.shared.executeCommand(commands[index]) { result in if result.starts(with: "Error:") { print("命令执行失败: \(commands[index])") print("错误信息: \(result)") diff --git a/Adobe Downloader/Utils/NewDownloadUtils.swift b/Adobe Downloader/Utils/NewDownloadUtils.swift index f0c8416..8c821bc 100644 --- a/Adobe Downloader/Utils/NewDownloadUtils.swift +++ b/Adobe Downloader/Utils/NewDownloadUtils.swift @@ -1364,7 +1364,7 @@ class NewDownloadUtils { private func executePrivilegedCommand(_ command: String) async -> String { return await withCheckedContinuation { continuation in - PrivilegedHelperManager.shared.executeCommand(command) { result in + ModernPrivilegedHelperManager.shared.executeCommand(command) { result in if result.starts(with: "Error:") { print("命令执行失败: \(command)") print("错误信息: \(result)") diff --git a/Adobe Downloader/Views/AboutView.swift b/Adobe Downloader/Views/AboutView.swift index 913116c..3b8af35 100644 --- a/Adobe Downloader/Views/AboutView.swift +++ b/Adobe Downloader/Views/AboutView.swift @@ -22,7 +22,6 @@ private enum AboutViewConstants { static let links: [(title: String, url: String)] = [ ("@X1a0He", "https://t.me/X1a0He_bot"), ("Github: Adobe Downloader", "https://github.com/X1a0He/Adobe-Downloader"), - ("QiuChenly: InjectLib", "https://github.com/QiuChenly/InjectLib") ] } @@ -239,7 +238,7 @@ final class GeneralSettingsViewModel: ObservableObject { self.helperConnectionStatus = .connecting - PrivilegedHelperManager.shared.$connectionState + ModernPrivilegedHelperManager.shared.$connectionState .receive(on: DispatchQueue.main) .sink { [weak self] state in switch state { @@ -819,6 +818,7 @@ struct HelperStatusRow: View { @Binding var helperAlertMessage: String @Binding var helperAlertSuccess: Bool @State private var isReinstallingHelper = false + @State private var helperStatus: ModernPrivilegedHelperManager.HelperStatus = .notInstalled var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -826,7 +826,7 @@ struct HelperStatusRow: View { Text("安装状态: ") .font(.system(size: 14, weight: .medium)) - if PrivilegedHelperManager.getHelperStatus { + if helperStatus == .installed { HStack(spacing: 5) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) @@ -863,12 +863,25 @@ struct HelperStatusRow: View { Button(action: { isReinstallingHelper = true - PrivilegedHelperManager.shared.removeInstallHelper() - PrivilegedHelperManager.shared.reinstallHelper { success, message in - helperAlertSuccess = success - helperAlertMessage = message - showHelperAlert = true - isReinstallingHelper = false + Task { + do { + try await ModernPrivilegedHelperManager.shared.uninstallHelper() + await ModernPrivilegedHelperManager.shared.checkAndInstallHelper() + + await MainActor.run { + helperAlertSuccess = true + helperAlertMessage = "Helper 重新安装成功" + showHelperAlert = true + isReinstallingHelper = false + } + } catch { + await MainActor.run { + helperAlertSuccess = false + helperAlertMessage = error.localizedDescription + showHelperAlert = true + isReinstallingHelper = false + } + } } }) { HStack(spacing: 4) { @@ -885,7 +898,7 @@ struct HelperStatusRow: View { .help("完全卸载并重新安装 Helper") } - if !PrivilegedHelperManager.getHelperStatus { + if helperStatus != .installed { Text("Helper 未安装将导致无法执行需要管理员权限的操作") .font(.caption) .foregroundColor(.red) @@ -914,12 +927,23 @@ struct HelperStatusRow: View { Spacer() Button(action: { - if PrivilegedHelperManager.getHelperStatus && + if helperStatus == .installed && viewModel.helperConnectionStatus != .connected { - PrivilegedHelperManager.shared.reconnectHelper { success, message in - helperAlertSuccess = success - helperAlertMessage = message - showHelperAlert = true + Task { + do { + try await ModernPrivilegedHelperManager.shared.reconnectHelper() + await MainActor.run { + helperAlertSuccess = true + helperAlertMessage = "重新连接成功" + showHelperAlert = true + } + } catch { + await MainActor.run { + helperAlertSuccess = false + helperAlertMessage = error.localizedDescription + showHelperAlert = true + } + } } } }) { @@ -937,6 +961,9 @@ struct HelperStatusRow: View { .help("尝试重新连接到已安装的 Helper") } } + .task { + helperStatus = await ModernPrivilegedHelperManager.shared.getHelperStatus() + } } private var helperStatusColor: Color { @@ -949,7 +976,7 @@ struct HelperStatusRow: View { } private var shouldDisableReconnectButton: Bool { - return !PrivilegedHelperManager.getHelperStatus || + return helperStatus != .installed || viewModel.helperConnectionStatus == .connected || isReinstallingHelper } diff --git a/Adobe Downloader/Views/CleanConfigView.swift b/Adobe Downloader/Views/CleanConfigView.swift index e4bddf1..200d1ec 100644 --- a/Adobe Downloader/Views/CleanConfigView.swift +++ b/Adobe Downloader/Views/CleanConfigView.swift @@ -11,6 +11,7 @@ struct CleanConfigView: View { @State private var showAlert = false @State private var alertMessage = "" @State private var chipInfo: String = "" + @State private var helperStatus: ModernPrivilegedHelperManager.HelperStatus = .notInstalled private func getChipInfo() -> String { var size = 0 @@ -83,6 +84,9 @@ struct CleanConfigView: View { .onAppear { chipInfo = getChipInfo() } + .task { + helperStatus = await ModernPrivilegedHelperManager.shared.getHelperStatus() + } } private func cleanConfig() { @@ -103,8 +107,8 @@ struct CleanConfigView: View { try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path) - if PrivilegedHelperManager.getHelperStatus { - PrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in + if helperStatus == .installed { + ModernPrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in if output.starts(with: "Error") { alertMessage = "清空配置失败: \(output)" showAlert = true diff --git a/Adobe Downloader/Views/CleanupView.swift b/Adobe Downloader/Views/CleanupView.swift index fc72ef1..622ca7f 100644 --- a/Adobe Downloader/Views/CleanupView.swift +++ b/Adobe Downloader/Views/CleanupView.swift @@ -338,7 +338,7 @@ struct CleanupView: View { } timeoutTimer.resume() - PrivilegedHelperManager.shared.executeCommand(command) { [self] output in + ModernPrivilegedHelperManager.shared.executeCommand(command) { [self] (output: String) in timeoutTimer.cancel() DispatchQueue.main.async { if let index = cleanupLogs.lastIndex(where: { $0.command == command }) { @@ -642,4 +642,4 @@ struct LogContentView: View { } } } -} \ No newline at end of file +} diff --git a/Adobe Downloader/Views/DownloadProgressView.swift b/Adobe Downloader/Views/DownloadProgressView.swift index 7b2acac..d6607db 100644 --- a/Adobe Downloader/Views/DownloadProgressView.swift +++ b/Adobe Downloader/Views/DownloadProgressView.swift @@ -128,7 +128,7 @@ struct DownloadProgressView: View { #if DEBUG Button(action: { do { - _ = try PrivilegedHelperManager.shared.getHelperProxy() + _ = try ModernPrivilegedHelperManager.shared.getHelperProxy() showInstallPrompt = false isInstalling = true Task { @@ -158,7 +158,7 @@ struct DownloadProgressView: View { if ModifySetup.isSetupModified() { Button(action: { do { - _ = try PrivilegedHelperManager.shared.getHelperProxy() + _ = try ModernPrivilegedHelperManager.shared.getHelperProxy() showInstallPrompt = false isInstalling = true Task { diff --git a/AdobeDownloaderHelperTool/Info.plist b/AdobeDownloaderHelperTool/Info.plist index c75c176..076fa85 100644 --- a/AdobeDownloaderHelperTool/Info.plist +++ b/AdobeDownloaderHelperTool/Info.plist @@ -5,7 +5,11 @@ CFBundleIdentifier com.x1a0he.macOS.Adobe-Downloader.helper CFBundleName - com.x1a0he.macOS.Adobe-Downloader.helper + Adobe Downloader Helper + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + CFBundleShortVersionString + $(MARKETING_VERSION) SMAuthorizedClients identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ diff --git a/AdobeDownloaderHelperTool/Launchd.plist b/AdobeDownloaderHelperTool/Launchd.plist index 263726d..2623adf 100644 --- a/AdobeDownloaderHelperTool/Launchd.plist +++ b/AdobeDownloaderHelperTool/Launchd.plist @@ -9,12 +9,8 @@ com.x1a0he.macOS.Adobe-Downloader.helper - Program - /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper - ProgramArguments - - /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper - + BundleProgram + Contents/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper RunAtLoad KeepAlive diff --git a/AdobeDownloaderHelperTool/com.x1a0he.macOS.Adobe-Downloader.helper.plist b/AdobeDownloaderHelperTool/com.x1a0he.macOS.Adobe-Downloader.helper.plist new file mode 100644 index 0000000..15a07b8 --- /dev/null +++ b/AdobeDownloaderHelperTool/com.x1a0he.macOS.Adobe-Downloader.helper.plist @@ -0,0 +1,23 @@ + + + + + Label + com.x1a0he.macOS.Adobe-Downloader.helper + MachServices + + com.x1a0he.macOS.Adobe-Downloader.helper + + + BundleProgram + Contents/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper + RunAtLoad + + KeepAlive + + StandardErrorPath + /tmp/com.x1a0he.macOS.Adobe-Downloader.helper.err + StandardOutPath + /tmp/com.x1a0he.macOS.Adobe-Downloader.helper.out + + \ No newline at end of file