From e2e3fb44ce0964ed2cd71ebc7043f9df30038e88 Mon Sep 17 00:00:00 2001 From: X1a0He Date: Sun, 17 Aug 2025 14:04:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9E=83=E5=9C=BE=20SMAppService=20?= =?UTF-8?q?=E8=B5=B6=E7=B4=A7=E6=BB=9A=EF=BC=8C=E5=9B=9E=E9=80=80=20SMJobB?= =?UTF-8?q?less=20Helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Adobe Downloader.xcodeproj/project.pbxproj | 10 +- Adobe Downloader/Adobe DownloaderApp.swift | 6 +- Adobe Downloader/AppDelegate.swift | 2 +- .../HelperMigrationManager.swift | 176 ---- .../ModernPrivilegedHelperManager.swift | 768 ----------------- .../PrivilegedHelperAdapter.swift | 123 +-- .../SMJobBlessHelperManager.swift | 798 ++++++++++++++++++ Adobe Downloader/Info.plist | 7 +- Adobe Downloader/Utils/InstallManager.swift | 10 +- Adobe Downloader/Utils/ModifySetup.swift | 8 +- Adobe Downloader/Utils/NewDownloadUtils.swift | 2 +- Adobe Downloader/Views/AboutView.swift | 69 +- Adobe Downloader/Views/CleanConfigView.swift | 8 +- Adobe Downloader/Views/CleanupView.swift | 2 +- .../Views/DownloadProgressView.swift | 4 +- AdobeDownloaderHelperTool/Info.plist | 2 +- AdobeDownloaderHelperTool/Launchd.plist | 8 +- AdobeDownloaderHelperTool/main.swift | 33 +- Localizables/Localizable.xcstrings | 52 +- 19 files changed, 935 insertions(+), 1153 deletions(-) delete mode 100644 Adobe Downloader/HelperManager/HelperMigrationManager.swift delete mode 100644 Adobe Downloader/HelperManager/ModernPrivilegedHelperManager.swift create mode 100644 Adobe Downloader/HelperManager/SMJobBlessHelperManager.swift diff --git a/Adobe Downloader.xcodeproj/project.pbxproj b/Adobe Downloader.xcodeproj/project.pbxproj index 5eaf9ce..91205be 100644 --- a/Adobe Downloader.xcodeproj/project.pbxproj +++ b/Adobe Downloader.xcodeproj/project.pbxproj @@ -24,7 +24,7 @@ 3C60E1C22CE3AA0B00600C07 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; - dstPath = Contents/Library/LaunchDaemons; + dstPath = Contents/Library/LaunchServices; dstSubfolderSpec = 1; files = ( 3C60E1C32CE3AA1B00600C07 /* com.x1a0he.macOS.Adobe-Downloader.helper in CopyFiles */, @@ -431,7 +431,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 212; + CURRENT_PROJECT_VERSION = 213; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\""; ENABLE_HARDENED_RUNTIME = NO; @@ -446,7 +446,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.1.3; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -464,7 +464,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 212; + CURRENT_PROJECT_VERSION = 213; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\""; ENABLE_HARDENED_RUNTIME = NO; @@ -479,7 +479,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.1.3; PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Adobe Downloader/Adobe DownloaderApp.swift b/Adobe Downloader/Adobe DownloaderApp.swift index 50ae032..47ee562 100644 --- a/Adobe Downloader/Adobe DownloaderApp.swift +++ b/Adobe Downloader/Adobe DownloaderApp.swift @@ -129,9 +129,9 @@ struct Adobe_DownloaderApp: App { } private func setupApplication() async { - Task { - await ModernPrivilegedHelperManager.shared.checkAndInstallHelper() - } + Task { + PrivilegedHelperAdapter.shared.checkInstall() + } await MainActor.run { globalNetworkManager.loadSavedTasks() diff --git a/Adobe Downloader/AppDelegate.swift b/Adobe Downloader/AppDelegate.swift index 84d058b..2670d65 100644 --- a/Adobe Downloader/AppDelegate.swift +++ b/Adobe Downloader/AppDelegate.swift @@ -34,7 +34,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { return event } - ModernPrivilegedHelperManager.shared.executeCommand("id -u") { _ in } + PrivilegedHelperAdapter.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 deleted file mode 100644 index 215db0f..0000000 --- a/Adobe Downloader/HelperManager/HelperMigrationManager.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// 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 deleted file mode 100644 index 8dc6e86..0000000 --- a/Adobe Downloader/HelperManager/ModernPrivilegedHelperManager.swift +++ /dev/null @@ -1,768 +0,0 @@ -// -// 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 - case needsApproval - - var description: String { - switch self { - case .connected: - return String(localized: "已连接") - case .disconnected: - return String(localized: "未连接") - case .connecting: - return String(localized: "正在连接") - case .needsApproval: - 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: 10.0, repeats: true) { [weak self] _ in - guard let self = self else { return } - if self.connectionState == .disconnected && self.shouldAutoReconnect && !self.isInitializing { - self.logger.info("尝试自动重连Helper...") - Task { - await self.attemptConnection() - } - } - } - } - - func checkAndInstallHelper() async { - isInitializing = true - logger.info("开始检查 Helper 状态") - - let status = await getHelperStatus() - - await MainActor.run { - switch status { - case .legacy: - handleLegacyInstallation() - break - case .notInstalled: - registerHelper() - break - case .needsApproval: - self.connectionState = .needsApproval - showApprovalGuidance() - break - case .requiresUpdate: - updateHelper() - break - case .installed: - Task { - let connectionResult = await attemptConnection() - if connectionResult { - logger.info("Helper 连接成功") - connectionSuccessBlock?() - } else { - logger.warning("Helper 安装成功但连接失败,可能存在权限问题") - await MainActor.run { - self.connectionState = .disconnected - } - } - self.isInitializing = false - } - } - } - } - - 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: - let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") - let installedVersion = UserDefaults.standard.string(forKey: "InstalledHelperVersion") - - if installedBuild == nil || installedVersion == nil { - logger.info("Helper已启用但缺少版本信息,需要重新注册") - return .requiresUpdate - } - - if await needsUpdate() { - return .requiresUpdate - } - return .installed - - case .requiresApproval: - logger.info("Helper需要用户批准") - return .needsApproval - - case .notFound: - return .notInstalled - - @unknown default: - logger.warning("未知的 SMAppService 状态: \(status.rawValue)") - return .needsApproval - } - } - - private func registerHelper() { - guard let appService = appService else { - logger.error("SMAppService 未初始化") - return - } - - logger.info("正在重新注册Helper...") - - do { - try appService.register() - logger.info("Helper 重新注册成功") - - if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { - UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild") - logger.info("已保存Helper Build版本: \(currentBuild)") - } - if let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { - UserDefaults.standard.set(currentVersion, forKey: "InstalledHelperVersion") - logger.info("已保存Helper主版本: \(currentVersion)") - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - Task { - await self.tryEnableHelper() - } - } - - } catch { - logger.error("Helper 注册失败: \(error)") - handleRegistrationError(error) - isInitializing = false - } - } - - private func tryEnableHelper() async { - guard let appService = appService else { return } - - let currentStatus = appService.status - logger.info("尝试启用Helper,当前状态: \(currentStatus.rawValue)") - - if currentStatus != .enabled { - do { - logger.info("尝试重新注册Helper以启用") - try appService.register() - - try await Task.sleep(nanoseconds: 2_000_000_000) - - let newStatus = appService.status - logger.info("重新注册后状态: \(newStatus.rawValue)") - - if newStatus == .enabled { - logger.info("Helper成功启用") - let connectionResult = await attemptConnection() - if connectionResult { - logger.info("Helper连接成功") - return - } - } - } catch { - logger.error("重新注册失败: \(error)") - } - } - - let connectionResult = await attemptConnection() - if !connectionResult { - logger.warning("Helper 注册成功但连接失败,需要用户批准") - await MainActor.run { - self.connectionState = .needsApproval - self.showApprovalGuidance() - } - } - - self.isInitializing = false - } - - private func updateHelper() { - logger.info("检测到Helper版本不匹配,开始重新安装Helper") - 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") - UserDefaults.standard.removeObject(forKey: "InstalledHelperVersion") - - 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: "whoami", path2: "", permissions: 0) { [weak self] result in - let trimmedResult = result.trimmingCharacters(in: .whitespacesAndNewlines) - self?.logger.info("Helper 响应: \(trimmedResult)") - - if trimmedResult == "root" { - isConnected = true - DispatchQueue.main.async { - self?.connection = newConnection - self?.connectionState = .connected - self?.logger.info("Helper 权限验证成功,当前用户: \(trimmedResult)") - } - } else if !trimmedResult.starts(with: "Error:") && !trimmedResult.isEmpty { - isConnected = true - DispatchQueue.main.async { - self?.connection = newConnection - self?.connectionState = .connected - self?.logger.warning("Helper 连接成功但权限可能不足,当前用户: \(trimmedResult)") - } - } else { - self?.logger.error("Helper 连接失败或权限不足: \(trimmedResult)") - } - semaphore.signal() - } - let waitResult = semaphore.wait(timeout: .now() + 8.0) - if waitResult == .timedOut { - logger.warning("Helper 连接超时") - } - } - - if !isConnected { - newConnection.invalidate() - DispatchQueue.main.async { - self.connectionState = .disconnected - } - return nil - } - - logger.info("XPC 连接建立成功") - return newConnection - } - - func disconnectHelper() async { - connectionQueue.sync { - connection?.invalidate() - connection = nil - - DispatchQueue.main.async { - self.connectionState = .disconnected - } - } - } - - func reconnectHelper() async throws { - logger.info("开始重新连接Helper") - shouldAutoReconnect = true - await disconnectHelper() - - try await Task.sleep(nanoseconds: 500_000_000) // 0.5秒 - - let connectionResult = await attemptConnection() - if connectionResult { - logger.info("重新连接成功") - } else { - logger.error("重新连接失败") - throw HelperError.connectionFailed - } - } - - func getHelperProxy() throws -> HelperToolProtocol { - if connectionState != .connected || connection == nil { - 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)") - DispatchQueue.main.async { - 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 { - DispatchQueue.main.async { [weak self] in - self?.connectionState = .disconnected - self?.logger.error("执行命令失败: \(error.localizedDescription)") - 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) { - DispatchQueue.main.async { [weak self] in - if result.starts(with: "Error:") { - self?.connectionState = .disconnected - self?.logger.warning("命令执行失败,连接状态设为断开: \(result)") - } else { - self?.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 currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { - logger.warning("无法获取当前应用版本信息") - return true - } - - let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") - let installedVersion = UserDefaults.standard.string(forKey: "InstalledHelperVersion") - - guard let savedBuild = installedBuild, let savedVersion = installedVersion else { - logger.info("未找到已安装的Helper版本信息,当前版本=\(currentVersion)(\(currentBuild))") - return true - } - - let versionComparison = compareVersions(current: currentVersion, installed: savedVersion) - if versionComparison == .orderedDescending { - logger.info("主版本升级,需要更新: 当前=\(currentVersion), 已安装=\(savedVersion)") - return true - } else if versionComparison == .orderedAscending { - logger.info("当前版本低于已安装版本,强制更新Helper以匹配应用: 当前=\(currentVersion), 已安装=\(savedVersion)") - return true - } - - let buildComparison = compareVersions(current: currentBuild, installed: savedBuild) - if buildComparison == .orderedDescending { - logger.info("Build版本升级,需要更新: 当前=\(currentBuild), 已安装=\(savedBuild)") - return true - } else if buildComparison == .orderedAscending { - logger.info("当前Build版本低于已安装版本,强制更新Helper以匹配应用: 当前=\(currentBuild), 已安装=\(savedBuild)") - return true - } - - logger.info("Helper版本检查通过: 当前版本=\(currentVersion)(\(currentBuild)), 已安装版本=\(savedVersion)(\(savedBuild))") - return false - } - - private func compareVersions(current: String, installed: String) -> ComparisonResult { - let currentComponents = current.split(separator: ".").compactMap { Int($0) } - let installedComponents = installed.split(separator: ".").compactMap { Int($0) } - - let maxCount = max(currentComponents.count, installedComponents.count) - - for i in 0.. installedPart { - return .orderedDescending - } else if currentPart < installedPart { - return .orderedAscending - } - } - - return .orderedSame - } - - @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需要后台Helper服务来执行安装和文件操作。 - - 解决方法: - 1. 点击下方「打开系统设置」按钮 - 2. 在「登录项与扩展」中找到 Adobe Downloader - 3. 确保应用已被允许,并检查是否有任何需要启用的后台项目 - 4. 如果看不到相关选项,请尝试: - - 重启 Adobe Downloader - - 或重启系统后再试 - - 注意:macOS可能需要重启才能完全激活Helper服务。 - """) - 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 index 3761486..4ec88b1 100644 --- a/Adobe Downloader/HelperManager/PrivilegedHelperAdapter.swift +++ b/Adobe Downloader/HelperManager/PrivilegedHelperAdapter.swift @@ -7,7 +7,7 @@ import Foundation import AppKit -import ServiceManagement +import Combine @objcMembers class PrivilegedHelperAdapter: NSObject, ObservableObject { @@ -17,7 +17,7 @@ class PrivilegedHelperAdapter: NSObject, ObservableObject { @Published var connectionState: ConnectionState = .disconnected - private let modernManager: ModernPrivilegedHelperManager + private let smJobBlessManager: SMJobBlessHelperManager var connectionSuccessBlock: (() -> Void)? enum HelperStatus { @@ -44,158 +44,95 @@ class PrivilegedHelperAdapter: NSObject, ObservableObject { } override init() { - self.modernManager = ModernPrivilegedHelperManager.shared + self.smJobBlessManager = SMJobBlessHelperManager.shared super.init() - modernManager.$connectionState + smJobBlessManager.$connectionState .receive(on: DispatchQueue.main) - .sink { [weak self] modernState in - self?.connectionState = self?.convertConnectionState(modernState) ?? .disconnected + .sink { [weak self] smJobBlessState in + self?.connectionState = self?.convertConnectionState(smJobBlessState) ?? .disconnected } .store(in: &cancellables) - Task { - await initializeWithMigration() + smJobBlessManager.connectionSuccessBlock = { [weak self] in + self?.connectionSuccessBlock?() } } private var cancellables = Set() func checkInstall() { - Task { - await modernManager.checkAndInstallHelper() - } + smJobBlessManager.checkInstall() } func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) { - Task { - let modernStatus = await modernManager.getHelperStatus() - let legacyStatus = convertHelperStatus(modernStatus) - - await MainActor.run { - callback(legacyStatus) - } + smJobBlessManager.getHelperStatus { status in + let legacyStatus = self.convertHelperStatus(status) + 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 + return SMJobBlessHelperManager.getHelperStatus } func executeCommand(_ command: String, completion: @escaping (String) -> Void) { - modernManager.executeCommand(command, completion: completion) + smJobBlessManager.executeCommand(command, completion: completion) } func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws { - try await modernManager.executeInstallation(command, progress: progress) + try await smJobBlessManager.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) - } - } + smJobBlessManager.reconnectHelper(completion: completion) } 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) - } - } + smJobBlessManager.reinstallHelper(completion: completion) } func removeInstallHelper(completion: ((Bool) -> Void)? = nil) { - Task { - do { - try await modernManager.uninstallHelper() - completion?(true) - } catch { - completion?(false) - } - } + smJobBlessManager.removeInstallHelper(completion: completion) } func forceReinstallHelper() { - reinstallHelper { _, _ in } + smJobBlessManager.forceCleanAndReinstallHelper { success, message in + print("Helper重新安装结果: \(success ? "成功" : "失败") - \(message)") + } } func disconnectHelper() { - Task { - await modernManager.disconnectHelper() - } + smJobBlessManager.disconnectHelper() } func uninstallHelperViaTerminal(completion: @escaping (Bool, String) -> Void) { - Task { - do { - try await modernManager.uninstallHelper() - completion(true, String(localized: "Helper 已完全卸载")) - } catch { - completion(false, error.localizedDescription) - } - } + smJobBlessManager.uninstallHelperViaTerminal(completion: completion) } public func getHelperProxy() throws -> HelperToolProtocol { - return try modernManager.getHelperProxy() + return try smJobBlessManager.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 { + private func convertConnectionState(_ smJobBlessState: SMJobBlessHelperManager.ConnectionState) -> ConnectionState { + switch smJobBlessState { case .connected: return .connected case .disconnected: return .disconnected case .connecting: return .connecting - case .needsApproval: - return .disconnected } } - private func convertHelperStatus(_ modernStatus: ModernPrivilegedHelperManager.HelperStatus) -> HelperStatus { - switch modernStatus { + private func convertHelperStatus(_ smJobBlessStatus: SMJobBlessHelperManager.HelperStatus) -> HelperStatus { + switch smJobBlessStatus { case .installed: return .installed - case .notInstalled, .needsApproval, .legacy: + case .noFound: return .noFound - case .requiresUpdate: + case .needUpdate: return .needUpdate } } } - -import Combine diff --git a/Adobe Downloader/HelperManager/SMJobBlessHelperManager.swift b/Adobe Downloader/HelperManager/SMJobBlessHelperManager.swift new file mode 100644 index 0000000..a757d19 --- /dev/null +++ b/Adobe Downloader/HelperManager/SMJobBlessHelperManager.swift @@ -0,0 +1,798 @@ +// +// SMJobBlessHelperManager.swift +// Adobe Downloader +// +// Created by X1a0He on 2025/08/17. +// + +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 SMJobBlessHelperManager: NSObject, ObservableObject { + + enum HelperStatus { + case installed + case noFound + case needUpdate + } + + static let shared = SMJobBlessHelperManager() + 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() + + 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 isInitializing { + return + } + + 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 Thread.isMainThread { + self.notifyInstall() + } else { + DispatchQueue.main.async { + self.notifyInstall() + } + } + case .needUpdate: + if Thread.isMainThread { + self.notifyInstall() + } else { + DispatchQueue.main.async { + self.notifyInstall() + } + } + case .installed: + if self.connectionState != .connected { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if let connection = self.connectToHelper() { + self.connection = connection + self.connectionState = .connected + self.connectionSuccessBlock?() + print("连接到已安装的Helper成功") + } else { + print("Helper已安装但连接失败") + } + } + } else { + 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, SMJobBlessHelperManager.machServiceName as CFString, authRef, &error) == false { + if let blessError = error?.takeRetainedValue() { + let nsError = blessError as Error as NSError + print("SMJobBless failed with error: \(blessError)") + print("Error domain: \(nsError.domain)") + print("Error code: \(nsError.code)") + print("Error description: \(nsError.localizedDescription)") + 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/" + SMJobBlessHelperManager.machServiceName) + guard + CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else { + reply(.noFound) + return + } + + let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(SMJobBlessHelperManager.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: "whoami", path2: "", permissions: 0) { result in + let trimmedResult = result.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmedResult == "root" { + completion(true, String(localized: "Helper 重新安装成功")) + } else if !trimmedResult.isEmpty && !trimmedResult.starts(with: "Error:") { + completion(true, String(localized: "Helper 重新安装成功,但权限不是root: \(trimmedResult)")) + } else { + print("Helper验证失败,返回结果: \(result)") + completion(false, String(localized: "Helper 安装失败: \(result)")) + } + } + } + + func removeInstallHelper(completion: ((Bool) -> Void)? = nil) { + if FileManager.default.fileExists(atPath: "/Library/LaunchDaemons/\(SMJobBlessHelperManager.machServiceName).plist") { + try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(SMJobBlessHelperManager.machServiceName).plist") + } + if FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(SMJobBlessHelperManager.machServiceName)") { + try? FileManager.default.removeItem(atPath: "/Library/PrivilegedHelperTools/\(SMJobBlessHelperManager.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: SMJobBlessHelperManager.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: "whoami", path2: "", permissions: 0) { [weak self] result in + let trimmedResult = result.trimmingCharacters(in: .whitespacesAndNewlines) + + if trimmedResult == "root" { + isConnected = true + DispatchQueue.main.async { + self?.connection = newConnection + self?.connectionState = .connected + } + } else if !trimmedResult.isEmpty && !trimmedResult.starts(with: "Error:") { + 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: .shellCommand, path1: "whoami", path2: "", permissions: 0) { result in + DispatchQueue.main.async { + let trimmedResult = result.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmedResult == "root" { + self.connectionState = .connected + completion(true, String(localized: "Helper 重新连接成功")) + } else if !trimmedResult.isEmpty && !trimmedResult.starts(with: "Error:") { + self.connectionState = .connected + completion(true, String(localized: "Helper 重新连接成功,但权限不是root: \(trimmedResult)")) + } 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 forceCleanAndReinstallHelper(completion: @escaping (Bool, String) -> Void) { + uninstallHelperViaTerminal { [weak self] success, message in + guard let self = self else { return } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + UserDefaults.standard.removeObject(forKey: "InstalledHelperBuild") + + self.isInitializing = false + self.shouldAutoReconnect = true + + let result = self.installHelperDaemon() + switch result { + case .success: + completion(true, String(localized: "Helper 重新安装成功")) + case .authorizationFail: + completion(false, String(localized: "获取授权失败")) + case .getAdminFail: + completion(false, String(localized: "获取管理员权限失败")) + case .blessError(let code): + completion(false, String(localized: "安装失败: \(result.alertContent)")) + } + } + } + } + + 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/\(SMJobBlessHelperManager.machServiceName).plist + sudo /bin/rm -f /Library/LaunchDaemons/\(SMJobBlessHelperManager.machServiceName).plist + sudo /bin/rm -f /Library/PrivilegedHelperTools/\(SMJobBlessHelperManager.machServiceName) + sudo /usr/bin/killall -u root -9 \(SMJobBlessHelperManager.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 SMJobBlessHelperManager { + private func notifyInstall() { + guard !isInitializing else { return } + + let result = installHelperDaemon() + if case .success = result { + shouldAutoReconnect = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + guard let self = self else { return } + if let connection = self.connectToHelper() { + self.connection = connection + self.connectionState = .connected + self.connectionSuccessBlock?() + } + } + return + } + result.alertAction() + let ret = result.shouldRetryLegacyWay() + useLegacyInstall = ret.0 + let isCancle = ret.1 + if !isCancle, useLegacyInstall { + shouldAutoReconnect = true + } 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/\(SMJobBlessHelperManager.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/\(SMJobBlessHelperManager.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 SMJobBlessHelperManager { + 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 409d01d..9891aff 100644 --- a/Adobe Downloader/Info.plist +++ b/Adobe Downloader/Info.plist @@ -9,13 +9,10 @@ NSAllowsArbitraryLoads - SMAppService + SMPrivilegedExecutables com.x1a0he.macOS.Adobe-Downloader.helper - - PlistName - com.x1a0he.macOS.Adobe-Downloader.helper.plist - + 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 70de67a..9ea09c8 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 - ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { result in + PrivilegedHelperAdapter.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 - ModernPrivilegedHelperManager.shared.executeCommand(removeCommand) { result in + PrivilegedHelperAdapter.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 ModernPrivilegedHelperManager.shared.executeInstallation(installCommand) { output in + try await PrivilegedHelperAdapter.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: "安装完成")) - ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } + PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { _ in } continuation.resume() return } else { @@ -241,7 +241,7 @@ actor InstallManager { } func cancel() { - ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in } + PrivilegedHelperAdapter.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 9f19f52..36c9370 100644 --- a/Adobe Downloader/Utils/ModifySetup.swift +++ b/Adobe Downloader/Utils/ModifySetup.swift @@ -84,14 +84,14 @@ class ModifySetup { if isSetupBackup() { print("检测到备份文件,尝试从备份恢复...") - ModernPrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(backupPath)' '\(setupPath)'") { result in + PrivilegedHelperAdapter.shared.executeCommand("/bin/cp -f '\(backupPath)' '\(setupPath)'") { result in if result.starts(with: "Error:") { print("从备份恢复失败: \(result)") } completion(!result.starts(with: "Error:")) } } else { - ModernPrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(setupPath)' '\(backupPath)'") { result in + PrivilegedHelperAdapter.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) { - ModernPrivilegedHelperManager.shared.executeCommand("/bin/chmod 644 '\(backupPath)'") { chmodResult in + PrivilegedHelperAdapter.shared.executeCommand("/bin/chmod 644 '\(backupPath)'") { chmodResult in if chmodResult.starts(with: "Error:") { print("设置备份文件权限失败: \(chmodResult)") } @@ -138,7 +138,7 @@ class ModifySetup { return } - ModernPrivilegedHelperManager.shared.executeCommand(commands[index]) { result in + PrivilegedHelperAdapter.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 8c821bc..d6e45b7 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 - ModernPrivilegedHelperManager.shared.executeCommand(command) { result in + PrivilegedHelperAdapter.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 13f21aa..d91638a 100644 --- a/Adobe Downloader/Views/AboutView.swift +++ b/Adobe Downloader/Views/AboutView.swift @@ -7,7 +7,6 @@ import SwiftUI import Sparkle import Combine -import ServiceManagement private enum AboutViewConstants { @@ -276,7 +275,6 @@ final class GeneralSettingsViewModel: ObservableObject { case connecting case disconnected case checking - case needsApproval } init(updater: SPUUpdater) { @@ -287,7 +285,7 @@ final class GeneralSettingsViewModel: ObservableObject { self.helperConnectionStatus = .connecting - ModernPrivilegedHelperManager.shared.$connectionState + PrivilegedHelperAdapter.shared.$connectionState .receive(on: DispatchQueue.main) .sink { [weak self] state in switch state { @@ -297,8 +295,6 @@ final class GeneralSettingsViewModel: ObservableObject { self?.helperConnectionStatus = .disconnected case .connecting: self?.helperConnectionStatus = .connecting - case .needsApproval: - self?.helperConnectionStatus = .needsApproval } } .store(in: &cancellables) @@ -874,7 +870,7 @@ struct HelperStatusRow: View { @Binding var helperAlertMessage: String @Binding var helperAlertSuccess: Bool @State private var isReinstallingHelper = false - @State private var helperStatus: ModernPrivilegedHelperManager.HelperStatus = .notInstalled + @State private var helperStatus: PrivilegedHelperAdapter.HelperStatus = .noFound var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -919,24 +915,12 @@ struct HelperStatusRow: View { Button(action: { isReinstallingHelper = true - 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 - } + PrivilegedHelperAdapter.shared.reinstallHelper { success, message in + DispatchQueue.main.async { + helperAlertSuccess = success + helperAlertMessage = message + showHelperAlert = true + isReinstallingHelper = false } } }) { @@ -983,32 +967,18 @@ struct HelperStatusRow: View { Spacer() Button(action: { - if viewModel.helperConnectionStatus == .needsApproval { - // 打开系统设置让用户批准Helper - SMAppService.openSystemSettingsLoginItems() - } else { - 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 - } - } + PrivilegedHelperAdapter.shared.reconnectHelper { success, message in + DispatchQueue.main.async { + helperAlertSuccess = success + helperAlertMessage = message + showHelperAlert = true } } }) { HStack(spacing: 4) { - Image(systemName: viewModel.helperConnectionStatus == .needsApproval ? "gear" : "network") + Image(systemName: "network") .font(.system(size: 12)) - Text(viewModel.helperConnectionStatus == .needsApproval ? "打开设置" : "重新连接") + Text("重新连接") .font(.system(size: 13)) } .frame(minWidth: 90) @@ -1016,11 +986,13 @@ struct HelperStatusRow: View { .buttonStyle(BeautifulButtonStyle(baseColor: shouldDisableReconnectButton ? Color.gray.opacity(0.6) : Color.blue.opacity(0.8))) .foregroundColor(shouldDisableReconnectButton ? Color.white.opacity(0.8) : .white) .disabled(shouldDisableReconnectButton) - .help(viewModel.helperConnectionStatus == .needsApproval ? "打开系统设置批准Helper" : "尝试重新连接到已安装的 Helper") + .help("尝试重新连接到已安装的 Helper") } } .task { - helperStatus = await ModernPrivilegedHelperManager.shared.getHelperStatus() + PrivilegedHelperAdapter.shared.getHelperStatus { status in + helperStatus = status + } } } @@ -1030,7 +1002,6 @@ struct HelperStatusRow: View { case .connecting: return .orange case .disconnected: return .red case .checking: return .orange - case .needsApproval: return .yellow } } @@ -1045,7 +1016,6 @@ struct HelperStatusRow: View { case .connecting: return Color.orange.opacity(0.1) case .disconnected: return Color.red.opacity(0.1) case .checking: return Color.orange.opacity(0.1) - case .needsApproval: return Color.yellow.opacity(0.1) } } @@ -1055,7 +1025,6 @@ struct HelperStatusRow: View { case .connecting: return String(localized: "正在连接") case .disconnected: return String(localized: "连接断开") case .checking: return String(localized: "检查中") - case .needsApproval: return String(localized: "需要批准") } } } diff --git a/Adobe Downloader/Views/CleanConfigView.swift b/Adobe Downloader/Views/CleanConfigView.swift index e4ff9eb..21bd16e 100644 --- a/Adobe Downloader/Views/CleanConfigView.swift +++ b/Adobe Downloader/Views/CleanConfigView.swift @@ -11,7 +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 + @State private var helperStatus: PrivilegedHelperAdapter.HelperStatus = .noFound private func getChipInfo() -> String { var size = 0 @@ -85,7 +85,9 @@ struct CleanConfigView: View { chipInfo = getChipInfo() } .task { - helperStatus = await ModernPrivilegedHelperManager.shared.getHelperStatus() + PrivilegedHelperAdapter.shared.getHelperStatus { status in + helperStatus = status + } } } @@ -108,7 +110,7 @@ struct CleanConfigView: View { ofItemAtPath: scriptURL.path) if helperStatus == .installed { - ModernPrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in + PrivilegedHelperAdapter.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in if output.starts(with: "Error") { alertMessage = String(localized: "清空配置失败: \(output)") showAlert = true diff --git a/Adobe Downloader/Views/CleanupView.swift b/Adobe Downloader/Views/CleanupView.swift index 622ca7f..995659f 100644 --- a/Adobe Downloader/Views/CleanupView.swift +++ b/Adobe Downloader/Views/CleanupView.swift @@ -338,7 +338,7 @@ struct CleanupView: View { } timeoutTimer.resume() - ModernPrivilegedHelperManager.shared.executeCommand(command) { [self] (output: String) in + PrivilegedHelperAdapter.shared.executeCommand(command) { [self] (output: String) in timeoutTimer.cancel() DispatchQueue.main.async { if let index = cleanupLogs.lastIndex(where: { $0.command == command }) { diff --git a/Adobe Downloader/Views/DownloadProgressView.swift b/Adobe Downloader/Views/DownloadProgressView.swift index d6607db..bd7430f 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 ModernPrivilegedHelperManager.shared.getHelperProxy() + _ = try PrivilegedHelperAdapter.shared.getHelperProxy() showInstallPrompt = false isInstalling = true Task { @@ -158,7 +158,7 @@ struct DownloadProgressView: View { if ModifySetup.isSetupModified() { Button(action: { do { - _ = try ModernPrivilegedHelperManager.shared.getHelperProxy() + _ = try PrivilegedHelperAdapter.shared.getHelperProxy() showInstallPrompt = false isInstalling = true Task { diff --git a/AdobeDownloaderHelperTool/Info.plist b/AdobeDownloaderHelperTool/Info.plist index 076fa85..93d9a26 100644 --- a/AdobeDownloaderHelperTool/Info.plist +++ b/AdobeDownloaderHelperTool/Info.plist @@ -5,7 +5,7 @@ CFBundleIdentifier com.x1a0he.macOS.Adobe-Downloader.helper CFBundleName - Adobe Downloader Helper + com.x1a0he.macOS.Adobe-Downloader.helper CFBundleVersion $(CURRENT_PROJECT_VERSION) CFBundleShortVersionString diff --git a/AdobeDownloaderHelperTool/Launchd.plist b/AdobeDownloaderHelperTool/Launchd.plist index 2623adf..263726d 100644 --- a/AdobeDownloaderHelperTool/Launchd.plist +++ b/AdobeDownloaderHelperTool/Launchd.plist @@ -9,8 +9,12 @@ com.x1a0he.macOS.Adobe-Downloader.helper - BundleProgram - Contents/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper + Program + /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper + ProgramArguments + + /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper + RunAtLoad KeepAlive diff --git a/AdobeDownloaderHelperTool/main.swift b/AdobeDownloaderHelperTool/main.swift index 5f3bd7e..5b8cf18 100644 --- a/AdobeDownloaderHelperTool/main.swift +++ b/AdobeDownloaderHelperTool/main.swift @@ -9,6 +9,12 @@ import os.log 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) +} + class SecureCommandHandler { static func createCommand(type: CommandType, path1: String, path2: String = "", permissions: Int = 0) -> String? { if type == .shellCommand { @@ -55,12 +61,6 @@ class SecureCommandHandler { } } -@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) -} - class HelperTool: NSObject, HelperToolProtocol { private let listener: NSXPCListener private var connections: Set = [] @@ -99,7 +99,7 @@ class HelperTool: NSObject, HelperToolProtocol { #if DEBUG self.logger.notice("收到安全命令执行请求: \(shellCommand, privacy: .public)") #else - self.logger.notice("收到安全命令执行请求") + self.logger.notice("收到安全命令执行请求: \(String(shellCommand.prefix(20)))") #endif let isSetupCommand = shellCommand.contains("Setup") && shellCommand.contains("--install") @@ -150,7 +150,9 @@ class HelperTool: NSObject, HelperToolProtocol { } let outputHandle = outputPipe.fileHandleForReading + let errorHandle = errorPipe.fileHandleForReading var output = "" + var errorOutput = "" outputHandle.readabilityHandler = { handle in let data = handle.availableData @@ -158,18 +160,27 @@ class HelperTool: NSObject, HelperToolProtocol { output += newOutput } } + + errorHandle.readabilityHandler = { handle in + let data = handle.availableData + if let newError = String(data: data, encoding: .utf8) { + errorOutput += newError + } + } task.waitUntilExit() outputHandle.readabilityHandler = nil - errorPipe.fileHandleForReading.readabilityHandler = nil + errorHandle.readabilityHandler = nil if task.terminationStatus == 0 { self.logger.notice("命令执行成功") - reply(output.isEmpty ? "Success" : output) + let result = output.isEmpty ? "Success" : output.trimmingCharacters(in: .whitespacesAndNewlines) + reply(result) } else { - self.logger.error("命令执行失败,退出码: \(task.terminationStatus, privacy: .public)") - reply("Error: Command failed with exit code \(task.terminationStatus)") + let fullErrorMsg = errorOutput.isEmpty ? "Unknown error" : errorOutput.trimmingCharacters(in: .whitespacesAndNewlines) + self.logger.error("命令执行失败,退出码: \(task.terminationStatus), 错误信息: \(fullErrorMsg)") + reply("Error: Command failed with exit code \(task.terminationStatus): \(fullErrorMsg)") } } } diff --git a/Localizables/Localizable.xcstrings b/Localizables/Localizable.xcstrings index b04d16e..470331f 100644 --- a/Localizables/Localizable.xcstrings +++ b/Localizables/Localizable.xcstrings @@ -100,6 +100,7 @@ } }, "%@ 执行失败" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -186,16 +187,6 @@ } } }, - "Adobe Downloader需要后台Helper服务来执行安装和文件操作。\n\n解决方法:\n1. 点击下方「打开系统设置」按钮\n2. 在「登录项与扩展」中找到 Adobe Downloader\n3. 确保应用已被允许,并检查是否有任何需要启用的后台项目\n4. 如果看不到相关选项,请尝试:\n - 重启 Adobe Downloader\n - 或重启系统后再试\n\n注意:macOS可能需要重启才能完全激活Helper服务。" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adobe Downloader needs to use the background service to install and move files. Please allow this app's background project in \"System Settings → General → Login Items and Extensions\"." - } - } - } - }, "Adobe Hosts" : { "localizations" : { "en" : { @@ -323,7 +314,6 @@ } }, "Helper 响应异常: %@" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -334,7 +324,6 @@ } }, "Helper 安装失败: %@" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -355,6 +344,7 @@ } }, "Helper 文件不存在或损坏,请重新安装应用" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -375,6 +365,7 @@ } }, "Helper 服务不可用" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -415,6 +406,7 @@ } }, "Helper 注册失败" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -435,7 +427,6 @@ } }, "Helper 重新安装成功" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -444,9 +435,11 @@ } } } + }, + "Helper 重新安装成功,但权限不是root: %@" : { + }, "Helper 重新连接成功" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -455,8 +448,12 @@ } } } + }, + "Helper 重新连接成功,但权限不是root: %@" : { + }, "Helper服务需要批准" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -482,6 +479,9 @@ }, "macOS %@" : { + }, + "OK" : { + }, "Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。" : { "localizations" : { @@ -1001,7 +1001,6 @@ } }, "准备卸载脚本失败: %@" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1142,6 +1141,7 @@ } }, "升级" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1152,7 +1152,6 @@ } }, "卸载失败: %@" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1374,7 +1373,6 @@ } }, "多次尝试连接失败" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1906,6 +1904,7 @@ } }, "打开系统设置" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1916,6 +1915,7 @@ } }, "打开系统设置批准Helper" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1926,6 +1926,7 @@ } }, "打开设置" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1936,7 +1937,6 @@ } }, "执行卸载脚本失败: %@" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2176,6 +2176,7 @@ } }, "新 Helper 服务验证失败" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2228,7 +2229,6 @@ } }, "无法获取Helper代理" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2500,6 +2500,7 @@ } }, "权限被拒绝,请检查应用签名和权限设置" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2561,6 +2562,7 @@ } }, "检测到旧版本安装,需要清理" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2571,6 +2573,7 @@ } }, "检测到旧版本的 Helper" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3255,6 +3258,7 @@ } }, "稍后设置" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3307,6 +3311,7 @@ } }, "系统检测到旧版本的 Adobe Downloader Helper,需要升级到新版本。这将需要管理员权限来清理旧安装。" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3440,7 +3445,6 @@ } }, "获取管理员授权失败,用户主动取消授权!" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3451,7 +3455,6 @@ } }, "获取管理员权限失败" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3515,6 +3518,7 @@ } }, "迁移完成,但需要在系统设置中批准新的 Helper 服务" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3565,7 +3569,6 @@ } }, "连接出现错误: %@" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3737,6 +3740,7 @@ } }, "重新安装失败" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3747,6 +3751,7 @@ } }, "重新安装成功" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3767,6 +3772,7 @@ } }, "重新连接成功" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3859,6 +3865,7 @@ } }, "需要在系统设置中批准" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3869,6 +3876,7 @@ } }, "需要批准" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : {