mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 03:14:57 +08:00
feat: 垃圾 SMAppService 赶紧滚,回退 SMJobBless Helper
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
3C60E1C22CE3AA0B00600C07 /* CopyFiles */ = {
|
3C60E1C22CE3AA0B00600C07 /* CopyFiles */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
dstPath = Contents/Library/LaunchDaemons;
|
dstPath = Contents/Library/LaunchServices;
|
||||||
dstSubfolderSpec = 1;
|
dstSubfolderSpec = 1;
|
||||||
files = (
|
files = (
|
||||||
3C60E1C32CE3AA1B00600C07 /* com.x1a0he.macOS.Adobe-Downloader.helper in CopyFiles */,
|
3C60E1C32CE3AA1B00600C07 /* com.x1a0he.macOS.Adobe-Downloader.helper in CopyFiles */,
|
||||||
@@ -431,7 +431,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 212;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 2.1.2;
|
MARKETING_VERSION = 2.1.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -464,7 +464,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 212;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\"";
|
||||||
ENABLE_HARDENED_RUNTIME = NO;
|
ENABLE_HARDENED_RUNTIME = NO;
|
||||||
@@ -479,7 +479,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 2.1.2;
|
MARKETING_VERSION = 2.1.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.x1a0he.macOS.Adobe-Downloader";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ struct Adobe_DownloaderApp: App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupApplication() async {
|
private func setupApplication() async {
|
||||||
Task {
|
Task {
|
||||||
await ModernPrivilegedHelperManager.shared.checkAndInstallHelper()
|
PrivilegedHelperAdapter.shared.checkInstall()
|
||||||
}
|
}
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
globalNetworkManager.loadSavedTasks()
|
globalNetworkManager.loadSavedTasks()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
ModernPrivilegedHelperManager.shared.executeCommand("id -u") { _ in }
|
PrivilegedHelperAdapter.shared.executeCommand("id -u") { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<AnyHashable>,
|
|
||||||
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<Void, Error>) 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<String, Error>) 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..<maxCount {
|
|
||||||
let currentPart = i < currentComponents.count ? currentComponents[i] : 0
|
|
||||||
let installedPart = i < installedComponents.count ? installedComponents[i] : 0
|
|
||||||
|
|
||||||
if currentPart > 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")
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
import ServiceManagement
|
import Combine
|
||||||
|
|
||||||
@objcMembers
|
@objcMembers
|
||||||
class PrivilegedHelperAdapter: NSObject, ObservableObject {
|
class PrivilegedHelperAdapter: NSObject, ObservableObject {
|
||||||
@@ -17,7 +17,7 @@ class PrivilegedHelperAdapter: NSObject, ObservableObject {
|
|||||||
|
|
||||||
@Published var connectionState: ConnectionState = .disconnected
|
@Published var connectionState: ConnectionState = .disconnected
|
||||||
|
|
||||||
private let modernManager: ModernPrivilegedHelperManager
|
private let smJobBlessManager: SMJobBlessHelperManager
|
||||||
var connectionSuccessBlock: (() -> Void)?
|
var connectionSuccessBlock: (() -> Void)?
|
||||||
|
|
||||||
enum HelperStatus {
|
enum HelperStatus {
|
||||||
@@ -44,158 +44,95 @@ class PrivilegedHelperAdapter: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.modernManager = ModernPrivilegedHelperManager.shared
|
self.smJobBlessManager = SMJobBlessHelperManager.shared
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
modernManager.$connectionState
|
smJobBlessManager.$connectionState
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] modernState in
|
.sink { [weak self] smJobBlessState in
|
||||||
self?.connectionState = self?.convertConnectionState(modernState) ?? .disconnected
|
self?.connectionState = self?.convertConnectionState(smJobBlessState) ?? .disconnected
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
Task {
|
smJobBlessManager.connectionSuccessBlock = { [weak self] in
|
||||||
await initializeWithMigration()
|
self?.connectionSuccessBlock?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
func checkInstall() {
|
func checkInstall() {
|
||||||
Task {
|
smJobBlessManager.checkInstall()
|
||||||
await modernManager.checkAndInstallHelper()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) {
|
func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) {
|
||||||
Task {
|
smJobBlessManager.getHelperStatus { status in
|
||||||
let modernStatus = await modernManager.getHelperStatus()
|
let legacyStatus = self.convertHelperStatus(status)
|
||||||
let legacyStatus = convertHelperStatus(modernStatus)
|
callback(legacyStatus)
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
callback(legacyStatus)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static var getHelperStatus: Bool {
|
static var getHelperStatus: Bool {
|
||||||
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + machServiceName)
|
return SMJobBlessHelperManager.getHelperStatus
|
||||||
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) {
|
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 {
|
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) {
|
func reconnectHelper(completion: @escaping (Bool, String) -> Void) {
|
||||||
Task {
|
smJobBlessManager.reconnectHelper(completion: completion)
|
||||||
do {
|
|
||||||
try await modernManager.reconnectHelper()
|
|
||||||
completion(true, String(localized: "重新连接成功"))
|
|
||||||
} catch {
|
|
||||||
completion(false, error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func reinstallHelper(completion: @escaping (Bool, String) -> Void) {
|
func reinstallHelper(completion: @escaping (Bool, String) -> Void) {
|
||||||
Task {
|
smJobBlessManager.reinstallHelper(completion: completion)
|
||||||
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) {
|
func removeInstallHelper(completion: ((Bool) -> Void)? = nil) {
|
||||||
Task {
|
smJobBlessManager.removeInstallHelper(completion: completion)
|
||||||
do {
|
|
||||||
try await modernManager.uninstallHelper()
|
|
||||||
completion?(true)
|
|
||||||
} catch {
|
|
||||||
completion?(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func forceReinstallHelper() {
|
func forceReinstallHelper() {
|
||||||
reinstallHelper { _, _ in }
|
smJobBlessManager.forceCleanAndReinstallHelper { success, message in
|
||||||
|
print("Helper重新安装结果: \(success ? "成功" : "失败") - \(message)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnectHelper() {
|
func disconnectHelper() {
|
||||||
Task {
|
smJobBlessManager.disconnectHelper()
|
||||||
await modernManager.disconnectHelper()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func uninstallHelperViaTerminal(completion: @escaping (Bool, String) -> Void) {
|
func uninstallHelperViaTerminal(completion: @escaping (Bool, String) -> Void) {
|
||||||
Task {
|
smJobBlessManager.uninstallHelperViaTerminal(completion: completion)
|
||||||
do {
|
|
||||||
try await modernManager.uninstallHelper()
|
|
||||||
completion(true, String(localized: "Helper 已完全卸载"))
|
|
||||||
} catch {
|
|
||||||
completion(false, error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getHelperProxy() throws -> HelperToolProtocol {
|
public func getHelperProxy() throws -> HelperToolProtocol {
|
||||||
return try modernManager.getHelperProxy()
|
return try smJobBlessManager.getHelperProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initializeWithMigration() async {
|
private func convertConnectionState(_ smJobBlessState: SMJobBlessHelperManager.ConnectionState) -> ConnectionState {
|
||||||
do {
|
switch smJobBlessState {
|
||||||
let _ = try await ModernPrivilegedHelperManager.initializeWithMigration()
|
|
||||||
connectionSuccessBlock?()
|
|
||||||
} catch {
|
|
||||||
print("Helper 初始化失败: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func convertConnectionState(_ modernState: ModernPrivilegedHelperManager.ConnectionState) -> ConnectionState {
|
|
||||||
switch modernState {
|
|
||||||
case .connected:
|
case .connected:
|
||||||
return .connected
|
return .connected
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
return .disconnected
|
return .disconnected
|
||||||
case .connecting:
|
case .connecting:
|
||||||
return .connecting
|
return .connecting
|
||||||
case .needsApproval:
|
|
||||||
return .disconnected
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func convertHelperStatus(_ modernStatus: ModernPrivilegedHelperManager.HelperStatus) -> HelperStatus {
|
private func convertHelperStatus(_ smJobBlessStatus: SMJobBlessHelperManager.HelperStatus) -> HelperStatus {
|
||||||
switch modernStatus {
|
switch smJobBlessStatus {
|
||||||
case .installed:
|
case .installed:
|
||||||
return .installed
|
return .installed
|
||||||
case .notInstalled, .needsApproval, .legacy:
|
case .noFound:
|
||||||
return .noFound
|
return .noFound
|
||||||
case .requiresUpdate:
|
case .needUpdate:
|
||||||
return .needUpdate
|
return .needUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import Combine
|
|
||||||
|
|||||||
798
Adobe Downloader/HelperManager/SMJobBlessHelperManager.swift
Normal file
798
Adobe Downloader/HelperManager/SMJobBlessHelperManager.swift
Normal file
@@ -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<CFError>?
|
||||||
|
|
||||||
|
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<AnyHashable>,
|
||||||
|
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<Void, Error>) 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<String, Error>) 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")
|
||||||
|
}
|
||||||
@@ -9,13 +9,10 @@
|
|||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SMAppService</key>
|
<key>SMPrivilegedExecutables</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
|
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
|
||||||
<dict>
|
<string>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 */</string>
|
||||||
<key>PlistName</key>
|
|
||||||
<string>com.x1a0he.macOS.Adobe-Downloader.helper.plist</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml</string>
|
<string>https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml</string>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ actor InstallManager {
|
|||||||
|
|
||||||
private func terminateSetupProcesses() async {
|
private func terminateSetupProcesses() async {
|
||||||
let _ = await withCheckedContinuation { continuation in
|
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)
|
continuation.resume(returning: result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ actor InstallManager {
|
|||||||
for logFile in logFiles {
|
for logFile in logFiles {
|
||||||
let removeCommand = "rm -f '\(logFile)'"
|
let removeCommand = "rm -f '\(logFile)'"
|
||||||
let result = await withCheckedContinuation { continuation in
|
let result = await withCheckedContinuation { continuation in
|
||||||
ModernPrivilegedHelperManager.shared.executeCommand(removeCommand) { result in
|
PrivilegedHelperAdapter.shared.executeCommand(removeCommand) { result in
|
||||||
continuation.resume(returning: result)
|
continuation.resume(returning: result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ actor InstallManager {
|
|||||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||||
Task.detached {
|
Task.detached {
|
||||||
do {
|
do {
|
||||||
try await ModernPrivilegedHelperManager.shared.executeInstallation(installCommand) { output in
|
try await PrivilegedHelperAdapter.shared.executeInstallation(installCommand) { output in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression),
|
if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression),
|
||||||
let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
|
let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
|
||||||
@@ -182,7 +182,7 @@ actor InstallManager {
|
|||||||
|
|
||||||
if exitCode == 0 {
|
if exitCode == 0 {
|
||||||
progressHandler(1.0, String(localized: "安装完成"))
|
progressHandler(1.0, String(localized: "安装完成"))
|
||||||
ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in }
|
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { _ in }
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@@ -241,7 +241,7 @@ actor InstallManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cancel() {
|
func cancel() {
|
||||||
ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in }
|
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInstallCommand(for driverPath: String) -> String {
|
func getInstallCommand(for driverPath: String) -> String {
|
||||||
|
|||||||
@@ -84,14 +84,14 @@ class ModifySetup {
|
|||||||
|
|
||||||
if isSetupBackup() {
|
if isSetupBackup() {
|
||||||
print("检测到备份文件,尝试从备份恢复...")
|
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:") {
|
if result.starts(with: "Error:") {
|
||||||
print("从备份恢复失败: \(result)")
|
print("从备份恢复失败: \(result)")
|
||||||
}
|
}
|
||||||
completion(!result.starts(with: "Error:"))
|
completion(!result.starts(with: "Error:"))
|
||||||
}
|
}
|
||||||
} else {
|
} 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:") {
|
if result.starts(with: "Error:") {
|
||||||
print("创建备份失败: \(result)")
|
print("创建备份失败: \(result)")
|
||||||
completion(false)
|
completion(false)
|
||||||
@@ -100,7 +100,7 @@ class ModifySetup {
|
|||||||
|
|
||||||
if !result.starts(with: "Error:") {
|
if !result.starts(with: "Error:") {
|
||||||
if FileManager.default.fileExists(atPath: backupPath) {
|
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:") {
|
if chmodResult.starts(with: "Error:") {
|
||||||
print("设置备份文件权限失败: \(chmodResult)")
|
print("设置备份文件权限失败: \(chmodResult)")
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ class ModifySetup {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ModernPrivilegedHelperManager.shared.executeCommand(commands[index]) { result in
|
PrivilegedHelperAdapter.shared.executeCommand(commands[index]) { result in
|
||||||
if result.starts(with: "Error:") {
|
if result.starts(with: "Error:") {
|
||||||
print("命令执行失败: \(commands[index])")
|
print("命令执行失败: \(commands[index])")
|
||||||
print("错误信息: \(result)")
|
print("错误信息: \(result)")
|
||||||
|
|||||||
@@ -1364,7 +1364,7 @@ class NewDownloadUtils {
|
|||||||
|
|
||||||
private func executePrivilegedCommand(_ command: String) async -> String {
|
private func executePrivilegedCommand(_ command: String) async -> String {
|
||||||
return await withCheckedContinuation { continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
ModernPrivilegedHelperManager.shared.executeCommand(command) { result in
|
PrivilegedHelperAdapter.shared.executeCommand(command) { result in
|
||||||
if result.starts(with: "Error:") {
|
if result.starts(with: "Error:") {
|
||||||
print("命令执行失败: \(command)")
|
print("命令执行失败: \(command)")
|
||||||
print("错误信息: \(result)")
|
print("错误信息: \(result)")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Sparkle
|
import Sparkle
|
||||||
import Combine
|
import Combine
|
||||||
import ServiceManagement
|
|
||||||
|
|
||||||
|
|
||||||
private enum AboutViewConstants {
|
private enum AboutViewConstants {
|
||||||
@@ -276,7 +275,6 @@ final class GeneralSettingsViewModel: ObservableObject {
|
|||||||
case connecting
|
case connecting
|
||||||
case disconnected
|
case disconnected
|
||||||
case checking
|
case checking
|
||||||
case needsApproval
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(updater: SPUUpdater) {
|
init(updater: SPUUpdater) {
|
||||||
@@ -287,7 +285,7 @@ final class GeneralSettingsViewModel: ObservableObject {
|
|||||||
|
|
||||||
self.helperConnectionStatus = .connecting
|
self.helperConnectionStatus = .connecting
|
||||||
|
|
||||||
ModernPrivilegedHelperManager.shared.$connectionState
|
PrivilegedHelperAdapter.shared.$connectionState
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] state in
|
.sink { [weak self] state in
|
||||||
switch state {
|
switch state {
|
||||||
@@ -297,8 +295,6 @@ final class GeneralSettingsViewModel: ObservableObject {
|
|||||||
self?.helperConnectionStatus = .disconnected
|
self?.helperConnectionStatus = .disconnected
|
||||||
case .connecting:
|
case .connecting:
|
||||||
self?.helperConnectionStatus = .connecting
|
self?.helperConnectionStatus = .connecting
|
||||||
case .needsApproval:
|
|
||||||
self?.helperConnectionStatus = .needsApproval
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@@ -874,7 +870,7 @@ struct HelperStatusRow: View {
|
|||||||
@Binding var helperAlertMessage: String
|
@Binding var helperAlertMessage: String
|
||||||
@Binding var helperAlertSuccess: Bool
|
@Binding var helperAlertSuccess: Bool
|
||||||
@State private var isReinstallingHelper = false
|
@State private var isReinstallingHelper = false
|
||||||
@State private var helperStatus: ModernPrivilegedHelperManager.HelperStatus = .notInstalled
|
@State private var helperStatus: PrivilegedHelperAdapter.HelperStatus = .noFound
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@@ -919,24 +915,12 @@ struct HelperStatusRow: View {
|
|||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
isReinstallingHelper = true
|
isReinstallingHelper = true
|
||||||
Task {
|
PrivilegedHelperAdapter.shared.reinstallHelper { success, message in
|
||||||
do {
|
DispatchQueue.main.async {
|
||||||
try await ModernPrivilegedHelperManager.shared.uninstallHelper()
|
helperAlertSuccess = success
|
||||||
await ModernPrivilegedHelperManager.shared.checkAndInstallHelper()
|
helperAlertMessage = message
|
||||||
|
showHelperAlert = true
|
||||||
await MainActor.run {
|
isReinstallingHelper = false
|
||||||
helperAlertSuccess = true
|
|
||||||
helperAlertMessage = "Helper 重新安装成功"
|
|
||||||
showHelperAlert = true
|
|
||||||
isReinstallingHelper = false
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
await MainActor.run {
|
|
||||||
helperAlertSuccess = false
|
|
||||||
helperAlertMessage = error.localizedDescription
|
|
||||||
showHelperAlert = true
|
|
||||||
isReinstallingHelper = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
@@ -983,32 +967,18 @@ struct HelperStatusRow: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if viewModel.helperConnectionStatus == .needsApproval {
|
PrivilegedHelperAdapter.shared.reconnectHelper { success, message in
|
||||||
// 打开系统设置让用户批准Helper
|
DispatchQueue.main.async {
|
||||||
SMAppService.openSystemSettingsLoginItems()
|
helperAlertSuccess = success
|
||||||
} else {
|
helperAlertMessage = message
|
||||||
Task {
|
showHelperAlert = true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: viewModel.helperConnectionStatus == .needsApproval ? "gear" : "network")
|
Image(systemName: "network")
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 12))
|
||||||
Text(viewModel.helperConnectionStatus == .needsApproval ? "打开设置" : "重新连接")
|
Text("重新连接")
|
||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
}
|
}
|
||||||
.frame(minWidth: 90)
|
.frame(minWidth: 90)
|
||||||
@@ -1016,11 +986,13 @@ struct HelperStatusRow: View {
|
|||||||
.buttonStyle(BeautifulButtonStyle(baseColor: shouldDisableReconnectButton ? Color.gray.opacity(0.6) : Color.blue.opacity(0.8)))
|
.buttonStyle(BeautifulButtonStyle(baseColor: shouldDisableReconnectButton ? Color.gray.opacity(0.6) : Color.blue.opacity(0.8)))
|
||||||
.foregroundColor(shouldDisableReconnectButton ? Color.white.opacity(0.8) : .white)
|
.foregroundColor(shouldDisableReconnectButton ? Color.white.opacity(0.8) : .white)
|
||||||
.disabled(shouldDisableReconnectButton)
|
.disabled(shouldDisableReconnectButton)
|
||||||
.help(viewModel.helperConnectionStatus == .needsApproval ? "打开系统设置批准Helper" : "尝试重新连接到已安装的 Helper")
|
.help("尝试重新连接到已安装的 Helper")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.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 .connecting: return .orange
|
||||||
case .disconnected: return .red
|
case .disconnected: return .red
|
||||||
case .checking: return .orange
|
case .checking: return .orange
|
||||||
case .needsApproval: return .yellow
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,7 +1016,6 @@ struct HelperStatusRow: View {
|
|||||||
case .connecting: return Color.orange.opacity(0.1)
|
case .connecting: return Color.orange.opacity(0.1)
|
||||||
case .disconnected: return Color.red.opacity(0.1)
|
case .disconnected: return Color.red.opacity(0.1)
|
||||||
case .checking: return Color.orange.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 .connecting: return String(localized: "正在连接")
|
||||||
case .disconnected: return String(localized: "连接断开")
|
case .disconnected: return String(localized: "连接断开")
|
||||||
case .checking: return String(localized: "检查中")
|
case .checking: return String(localized: "检查中")
|
||||||
case .needsApproval: return String(localized: "需要批准")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ struct CleanConfigView: View {
|
|||||||
@State private var showAlert = false
|
@State private var showAlert = false
|
||||||
@State private var alertMessage = ""
|
@State private var alertMessage = ""
|
||||||
@State private var chipInfo: String = ""
|
@State private var chipInfo: String = ""
|
||||||
@State private var helperStatus: ModernPrivilegedHelperManager.HelperStatus = .notInstalled
|
@State private var helperStatus: PrivilegedHelperAdapter.HelperStatus = .noFound
|
||||||
|
|
||||||
private func getChipInfo() -> String {
|
private func getChipInfo() -> String {
|
||||||
var size = 0
|
var size = 0
|
||||||
@@ -85,7 +85,9 @@ struct CleanConfigView: View {
|
|||||||
chipInfo = getChipInfo()
|
chipInfo = getChipInfo()
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
helperStatus = await ModernPrivilegedHelperManager.shared.getHelperStatus()
|
PrivilegedHelperAdapter.shared.getHelperStatus { status in
|
||||||
|
helperStatus = status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ struct CleanConfigView: View {
|
|||||||
ofItemAtPath: scriptURL.path)
|
ofItemAtPath: scriptURL.path)
|
||||||
|
|
||||||
if helperStatus == .installed {
|
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") {
|
if output.starts(with: "Error") {
|
||||||
alertMessage = String(localized: "清空配置失败: \(output)")
|
alertMessage = String(localized: "清空配置失败: \(output)")
|
||||||
showAlert = true
|
showAlert = true
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ struct CleanupView: View {
|
|||||||
}
|
}
|
||||||
timeoutTimer.resume()
|
timeoutTimer.resume()
|
||||||
|
|
||||||
ModernPrivilegedHelperManager.shared.executeCommand(command) { [self] (output: String) in
|
PrivilegedHelperAdapter.shared.executeCommand(command) { [self] (output: String) in
|
||||||
timeoutTimer.cancel()
|
timeoutTimer.cancel()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let index = cleanupLogs.lastIndex(where: { $0.command == command }) {
|
if let index = cleanupLogs.lastIndex(where: { $0.command == command }) {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ struct DownloadProgressView: View {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
Button(action: {
|
Button(action: {
|
||||||
do {
|
do {
|
||||||
_ = try ModernPrivilegedHelperManager.shared.getHelperProxy()
|
_ = try PrivilegedHelperAdapter.shared.getHelperProxy()
|
||||||
showInstallPrompt = false
|
showInstallPrompt = false
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
Task {
|
Task {
|
||||||
@@ -158,7 +158,7 @@ struct DownloadProgressView: View {
|
|||||||
if ModifySetup.isSetupModified() {
|
if ModifySetup.isSetupModified() {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
do {
|
do {
|
||||||
_ = try ModernPrivilegedHelperManager.shared.getHelperProxy()
|
_ = try PrivilegedHelperAdapter.shared.getHelperProxy()
|
||||||
showInstallPrompt = false
|
showInstallPrompt = false
|
||||||
isInstalling = true
|
isInstalling = true
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
|
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Adobe Downloader Helper</string>
|
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
@@ -9,8 +9,12 @@
|
|||||||
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
|
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>BundleProgram</key>
|
<key>Program</key>
|
||||||
<string>Contents/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper</string>
|
<string>/Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper</string>
|
||||||
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>KeepAlive</key>
|
<key>KeepAlive</key>
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import os.log
|
|||||||
case shellCommand
|
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 {
|
class SecureCommandHandler {
|
||||||
static func createCommand(type: CommandType, path1: String, path2: String = "", permissions: Int = 0) -> String? {
|
static func createCommand(type: CommandType, path1: String, path2: String = "", permissions: Int = 0) -> String? {
|
||||||
if type == .shellCommand {
|
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 {
|
class HelperTool: NSObject, HelperToolProtocol {
|
||||||
private let listener: NSXPCListener
|
private let listener: NSXPCListener
|
||||||
private var connections: Set<NSXPCConnection> = []
|
private var connections: Set<NSXPCConnection> = []
|
||||||
@@ -99,7 +99,7 @@ class HelperTool: NSObject, HelperToolProtocol {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
self.logger.notice("收到安全命令执行请求: \(shellCommand, privacy: .public)")
|
self.logger.notice("收到安全命令执行请求: \(shellCommand, privacy: .public)")
|
||||||
#else
|
#else
|
||||||
self.logger.notice("收到安全命令执行请求")
|
self.logger.notice("收到安全命令执行请求: \(String(shellCommand.prefix(20)))")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
let isSetupCommand = shellCommand.contains("Setup") && shellCommand.contains("--install")
|
let isSetupCommand = shellCommand.contains("Setup") && shellCommand.contains("--install")
|
||||||
@@ -150,7 +150,9 @@ class HelperTool: NSObject, HelperToolProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let outputHandle = outputPipe.fileHandleForReading
|
let outputHandle = outputPipe.fileHandleForReading
|
||||||
|
let errorHandle = errorPipe.fileHandleForReading
|
||||||
var output = ""
|
var output = ""
|
||||||
|
var errorOutput = ""
|
||||||
|
|
||||||
outputHandle.readabilityHandler = { handle in
|
outputHandle.readabilityHandler = { handle in
|
||||||
let data = handle.availableData
|
let data = handle.availableData
|
||||||
@@ -158,18 +160,27 @@ class HelperTool: NSObject, HelperToolProtocol {
|
|||||||
output += newOutput
|
output += newOutput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorHandle.readabilityHandler = { handle in
|
||||||
|
let data = handle.availableData
|
||||||
|
if let newError = String(data: data, encoding: .utf8) {
|
||||||
|
errorOutput += newError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
task.waitUntilExit()
|
task.waitUntilExit()
|
||||||
|
|
||||||
outputHandle.readabilityHandler = nil
|
outputHandle.readabilityHandler = nil
|
||||||
errorPipe.fileHandleForReading.readabilityHandler = nil
|
errorHandle.readabilityHandler = nil
|
||||||
|
|
||||||
if task.terminationStatus == 0 {
|
if task.terminationStatus == 0 {
|
||||||
self.logger.notice("命令执行成功")
|
self.logger.notice("命令执行成功")
|
||||||
reply(output.isEmpty ? "Success" : output)
|
let result = output.isEmpty ? "Success" : output.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
reply(result)
|
||||||
} else {
|
} else {
|
||||||
self.logger.error("命令执行失败,退出码: \(task.terminationStatus, privacy: .public)")
|
let fullErrorMsg = errorOutput.isEmpty ? "Unknown error" : errorOutput.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
reply("Error: Command failed with exit code \(task.terminationStatus)")
|
self.logger.error("命令执行失败,退出码: \(task.terminationStatus), 错误信息: \(fullErrorMsg)")
|
||||||
|
reply("Error: Command failed with exit code \(task.terminationStatus): \(fullErrorMsg)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"%@ 执行失败" : {
|
"%@ 执行失败" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"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" : {
|
"Adobe Hosts" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -323,7 +314,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Helper 响应异常: %@" : {
|
"Helper 响应异常: %@" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -334,7 +324,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Helper 安装失败: %@" : {
|
"Helper 安装失败: %@" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -355,6 +344,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Helper 文件不存在或损坏,请重新安装应用" : {
|
"Helper 文件不存在或损坏,请重新安装应用" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -375,6 +365,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Helper 服务不可用" : {
|
"Helper 服务不可用" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -415,6 +406,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Helper 注册失败" : {
|
"Helper 注册失败" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -435,7 +427,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Helper 重新安装成功" : {
|
"Helper 重新安装成功" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -444,9 +435,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Helper 重新安装成功,但权限不是root: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Helper 重新连接成功" : {
|
"Helper 重新连接成功" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -455,8 +448,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Helper 重新连接成功,但权限不是root: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Helper服务需要批准" : {
|
"Helper服务需要批准" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -482,6 +479,9 @@
|
|||||||
},
|
},
|
||||||
"macOS %@" : {
|
"macOS %@" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"OK" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。" : {
|
"Setup 组件是 Adobe 官方的安装程序组件,我们需要对其进行修改以实现绕过验证的功能。如果没有下载并处理 Setup 组件,将无法使用安装功能。" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1001,7 +1001,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"准备卸载脚本失败: %@" : {
|
"准备卸载脚本失败: %@" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1142,6 +1141,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"升级" : {
|
"升级" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1152,7 +1152,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"卸载失败: %@" : {
|
"卸载失败: %@" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1374,7 +1373,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"多次尝试连接失败" : {
|
"多次尝试连接失败" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1906,6 +1904,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"打开系统设置" : {
|
"打开系统设置" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1916,6 +1915,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"打开系统设置批准Helper" : {
|
"打开系统设置批准Helper" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1926,6 +1926,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"打开设置" : {
|
"打开设置" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1936,7 +1937,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"执行卸载脚本失败: %@" : {
|
"执行卸载脚本失败: %@" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2176,6 +2176,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"新 Helper 服务验证失败" : {
|
"新 Helper 服务验证失败" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2228,7 +2229,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"无法获取Helper代理" : {
|
"无法获取Helper代理" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2500,6 +2500,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"权限被拒绝,请检查应用签名和权限设置" : {
|
"权限被拒绝,请检查应用签名和权限设置" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2561,6 +2562,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"检测到旧版本安装,需要清理" : {
|
"检测到旧版本安装,需要清理" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2571,6 +2573,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"检测到旧版本的 Helper" : {
|
"检测到旧版本的 Helper" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3255,6 +3258,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"稍后设置" : {
|
"稍后设置" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3307,6 +3311,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"系统检测到旧版本的 Adobe Downloader Helper,需要升级到新版本。这将需要管理员权限来清理旧安装。" : {
|
"系统检测到旧版本的 Adobe Downloader Helper,需要升级到新版本。这将需要管理员权限来清理旧安装。" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3440,7 +3445,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"获取管理员授权失败,用户主动取消授权!" : {
|
"获取管理员授权失败,用户主动取消授权!" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3451,7 +3455,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"获取管理员权限失败" : {
|
"获取管理员权限失败" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3515,6 +3518,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"迁移完成,但需要在系统设置中批准新的 Helper 服务" : {
|
"迁移完成,但需要在系统设置中批准新的 Helper 服务" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3565,7 +3569,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"连接出现错误: %@" : {
|
"连接出现错误: %@" : {
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3737,6 +3740,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"重新安装失败" : {
|
"重新安装失败" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3747,6 +3751,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"重新安装成功" : {
|
"重新安装成功" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3767,6 +3772,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"重新连接成功" : {
|
"重新连接成功" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3859,6 +3865,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"需要在系统设置中批准" : {
|
"需要在系统设置中批准" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3869,6 +3876,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"需要批准" : {
|
"需要批准" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
Reference in New Issue
Block a user