feat: 吃了更多的西瓜,重写Helper,使用macOS 13.0+的 SMAppService 实现

This commit is contained in:
X1a0He
2025-07-20 01:38:27 +08:00
parent fbf1dc72b8
commit 77ef2e6100
17 changed files with 1080 additions and 801 deletions

View File

@@ -129,8 +129,10 @@ struct Adobe_DownloaderApp: App {
}
private func setupApplication() async {
PrivilegedHelperManager.shared.checkInstall()
Task {
await ModernPrivilegedHelperManager.shared.checkAndInstallHelper()
}
await MainActor.run {
globalNetworkManager.loadSavedTasks()
}

View File

@@ -34,7 +34,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return event
}
PrivilegedHelperManager.shared.executeCommand("id -u") { _ in }
ModernPrivilegedHelperManager.shared.executeCommand("id -u") { _ in }
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {

View File

@@ -0,0 +1,176 @@
//
// HelperMigrationManager.swift
// Adobe Downloader
//
// Created by X1a0He on 2025/07/20.
//
import Foundation
import ServiceManagement
import os.log
class HelperMigrationManager {
private let logger = Logger(subsystem: "com.x1a0he.macOS.Adobe-Downloader", category: "Migration")
static func performMigrationIfNeeded() async throws {
let migrationManager = HelperMigrationManager()
if migrationManager.needsMigration() {
try await migrationManager.performMigration()
}
}
private func needsMigration() -> Bool {
// SMJobBless
let legacyPlistPath = "/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist"
let legacyHelperPath = "/Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper"
let hasLegacyFiles = FileManager.default.fileExists(atPath: legacyPlistPath) ||
FileManager.default.fileExists(atPath: legacyHelperPath)
let appService = SMAppService.daemon(plistName: "com.x1a0he.macOS.Adobe-Downloader.helper")
let hasModernService = appService.status != .notRegistered
logger.info("迁移检查 - 旧文件存在: \(hasLegacyFiles), 新服务已注册: \(hasModernService)")
return hasLegacyFiles && !hasModernService
}
private func performMigration() async throws {
logger.info("开始 Helper 迁移过程")
try await stopLegacyService()
try await cleanupLegacyFiles()
try await registerModernService()
try await validateNewService()
logger.info("Helper 迁移完成")
}
private func stopLegacyService() async throws {
logger.info("停止旧的 Helper 服务")
let script = """
#!/bin/bash
# 停止旧的 LaunchDaemon
sudo /bin/launchctl unload /Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist 2>/dev/null || true
# 终止可能运行的进程
sudo /usr/bin/killall -u root -9 com.x1a0he.macOS.Adobe-Downloader.helper 2>/dev/null || true
exit 0
"""
try await executePrivilegedScript(script, description: "停止旧服务")
}
private func cleanupLegacyFiles() async throws {
logger.info("清理旧的安装文件")
let script = """
#!/bin/bash
# 删除旧的 plist 文件
sudo /bin/rm -f /Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper.plist
# 删除旧的 Helper 文件
sudo /bin/rm -f /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper
exit 0
"""
try await executePrivilegedScript(script, description: "清理旧文件")
}
private func registerModernService() async throws {
logger.info("注册新的 SMAppService")
let modernManager = ModernPrivilegedHelperManager.shared
await modernManager.checkAndInstallHelper()
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
}
private func validateNewService() async throws {
logger.info("验证新服务")
let modernManager = ModernPrivilegedHelperManager.shared
let status = await modernManager.getHelperStatus()
switch status {
case .installed:
logger.info("新服务验证成功")
case .needsApproval:
logger.warning("新服务需要用户批准")
throw MigrationError.requiresUserApproval
default:
logger.error("新服务验证失败: \(String(describing: status))")
throw MigrationError.validationFailed
}
}
private func executePrivilegedScript(_ script: String, description: String) async throws {
let tempDir = FileManager.default.temporaryDirectory
let scriptURL = tempDir.appendingPathComponent("migration_\(UUID().uuidString).sh")
try script.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path)
defer {
try? FileManager.default.removeItem(at: scriptURL)
}
return try await withCheckedThrowingContinuation { continuation in
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
task.arguments = ["-e", "do shell script \"\(scriptURL.path)\" with administrator privileges"]
do {
try task.run()
task.waitUntilExit()
if task.terminationStatus == 0 {
self.logger.info("\(description) 执行成功")
continuation.resume()
} else {
self.logger.error("\(description) 执行失败: \(task.terminationStatus)")
continuation.resume(throwing: MigrationError.scriptExecutionFailed(description))
}
} catch {
self.logger.error("\(description) 启动失败: \(error)")
continuation.resume(throwing: error)
}
}
}
}
enum MigrationError: LocalizedError {
case requiresUserApproval
case validationFailed
case scriptExecutionFailed(String)
var errorDescription: String? {
switch self {
case .requiresUserApproval:
return String(localized: "迁移完成,但需要在系统设置中批准新的 Helper 服务")
case .validationFailed:
return String(localized: "新 Helper 服务验证失败")
case .scriptExecutionFailed(let description):
return String(localized: "\(description) 执行失败")
}
}
}
extension ModernPrivilegedHelperManager {
static func initializeWithMigration() async throws -> ModernPrivilegedHelperManager {
try await HelperMigrationManager.performMigrationIfNeeded()
let manager = ModernPrivilegedHelperManager.shared
await manager.checkAndInstallHelper()
return manager
}
}

View File

@@ -0,0 +1,605 @@
//
// ModernPrivilegedHelperManager.swift
// Adobe Downloader
//
// Created by X1a0He on 2025/07/20.
//
import AppKit
import Cocoa
import ServiceManagement
import os.log
@objc enum CommandType: Int {
case install
case uninstall
case moveFile
case setPermissions
case shellCommand
}
@objc(HelperToolProtocol) protocol HelperToolProtocol {
@objc(executeCommand:path1:path2:permissions:withReply:)
func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void)
func getInstallationOutput(withReply reply: @escaping (String) -> Void)
}
@objcMembers
class ModernPrivilegedHelperManager: NSObject, ObservableObject {
enum HelperStatus {
case installed
case notInstalled
case needsApproval
case requiresUpdate
case legacy
}
enum ConnectionState {
case connected
case disconnected
case connecting
var description: String {
switch self {
case .connected:
return String(localized: "已连接")
case .disconnected:
return String(localized: "未连接")
case .connecting:
return String(localized: "正在连接")
}
}
}
enum HelperError: LocalizedError {
case serviceUnavailable
case connectionFailed
case proxyError
case authorizationFailed
case installationFailed(String)
case legacyInstallationDetected
var errorDescription: String? {
switch self {
case .serviceUnavailable:
return String(localized: "Helper 服务不可用")
case .connectionFailed:
return String(localized: "无法连接到 Helper")
case .proxyError:
return String(localized: "无法获取 Helper 代理")
case .authorizationFailed:
return String(localized: "获取授权失败")
case .installationFailed(let reason):
return String(localized: "安装失败: \(reason)")
case .legacyInstallationDetected:
return String(localized: "检测到旧版本安装,需要清理")
}
}
}
static let shared = ModernPrivilegedHelperManager()
static let helperIdentifier = "com.x1a0he.macOS.Adobe-Downloader.helper"
private let logger = Logger(subsystem: "com.x1a0he.macOS.Adobe-Downloader", category: "HelperManager")
@Published private(set) var connectionState: ConnectionState = .disconnected
private var appService: SMAppService?
private var connection: NSXPCConnection?
private let connectionQueue = DispatchQueue(label: "com.x1a0he.macOS.Adobe-Downloader.helper.connection")
var connectionSuccessBlock: (() -> Void)?
private var shouldAutoReconnect = true
private var isInitializing = false
override init() {
super.init()
initializeAppService()
setupAutoReconnect()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleConnectionInvalidation),
name: .NSXPCConnectionInvalid,
object: nil
)
}
private func initializeAppService() {
appService = SMAppService.daemon(plistName: "com.x1a0he.macOS.Adobe-Downloader.helper.plist")
}
private func setupAutoReconnect() {
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
if self.connectionState == .disconnected && self.shouldAutoReconnect {
Task {
await self.attemptConnection()
}
}
}
}
func checkAndInstallHelper() async {
logger.info("开始检查 Helper 状态")
let status = await getHelperStatus()
await MainActor.run {
switch status {
case .legacy:
handleLegacyInstallation()
break
case .notInstalled:
registerHelper()
break
case .needsApproval:
showApprovalGuidance()
break
case .requiresUpdate:
updateHelper()
break
case .installed:
Task {
await attemptConnection()
connectionSuccessBlock?()
}
}
}
}
func getHelperStatus() async -> HelperStatus {
guard let appService = appService else {
return .notInstalled
}
if hasLegacyInstallation() {
return .legacy
}
let status = appService.status
logger.info("SMAppService 状态: \(status.rawValue)")
switch status {
case .notRegistered:
return .notInstalled
case .enabled:
if await needsUpdate() {
return .requiresUpdate
}
return .installed
case .requiresApproval:
return .needsApproval
case .notFound:
return .notInstalled
@unknown default:
logger.warning("未知的 SMAppService 状态: \(status.rawValue)")
return .notInstalled
}
}
private func registerHelper() {
guard let appService = appService else {
logger.error("SMAppService 未初始化")
return
}
do {
try appService.register()
logger.info("Helper 注册成功")
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
Task {
await self.attemptConnection()
}
}
} catch {
logger.error("Helper 注册失败: \(error)")
handleRegistrationError(error)
}
}
private func updateHelper() {
registerHelper()
}
func uninstallHelper() async throws {
shouldAutoReconnect = false
await disconnectHelper()
if let appService = appService {
do {
try await appService.unregister()
logger.info("SMAppService 卸载成功")
} catch {
logger.error("SMAppService 卸载失败: \(error)")
throw error
}
}
try await cleanupLegacyInstallation()
UserDefaults.standard.removeObject(forKey: "InstalledHelperBuild")
await MainActor.run {
connectionState = .disconnected
}
}
@discardableResult
private func attemptConnection() async -> Bool {
return connectionQueue.sync {
createConnection() != nil
}
}
private func createConnection() -> NSXPCConnection? {
DispatchQueue.main.async {
self.connectionState = .connecting
}
if let existingConnection = connection {
existingConnection.invalidate()
connection = nil
}
let newConnection = NSXPCConnection(machServiceName: Self.helperIdentifier, options: .privileged)
let interface = NSXPCInterface(with: HelperToolProtocol.self)
interface.setClasses(
NSSet(array: [NSString.self, NSNumber.self]) as! Set<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: "id -u", path2: "", permissions: 0) { [weak self] result in
if result.contains("0") || result == "0" {
isConnected = true
DispatchQueue.main.async {
self?.connection = newConnection
self?.connectionState = .connected
}
}
semaphore.signal()
}
_ = semaphore.wait(timeout: .now() + 1.0)
}
if !isConnected {
newConnection.invalidate()
DispatchQueue.main.async {
self.connectionState = .disconnected
}
return nil
}
logger.info("XPC 连接建立成功")
return newConnection
}
func disconnectHelper() async {
connectionQueue.sync {
shouldAutoReconnect = false
connection?.invalidate()
connection = nil
DispatchQueue.main.async {
self.connectionState = .disconnected
}
}
}
func reconnectHelper() async throws {
await disconnectHelper()
shouldAutoReconnect = true
if await attemptConnection() {
logger.info("重新连接成功")
} else {
throw HelperError.connectionFailed
}
}
func getHelperProxy() throws -> HelperToolProtocol {
if connectionState != .connected {
guard let newConnection = connectionQueue.sync(execute: { createConnection() }) else {
throw HelperError.connectionFailed
}
connection = newConnection
}
guard let helper = connection?.remoteObjectProxyWithErrorHandler({ [weak self] error in
self?.logger.error("XPC 代理错误: \(error)")
self?.connectionState = .disconnected
}) as? HelperToolProtocol else {
throw HelperError.proxyError
}
return helper
}
func executeCommand(_ command: String, completion: @escaping (String) -> Void) {
do {
let helper = try getHelperProxy()
if command.contains("perl") || command.contains("codesign") || command.contains("xattr") {
helper.executeCommand(type: .shellCommand, path1: command, path2: "", permissions: 0) { [weak self] result in
DispatchQueue.main.async {
self?.updateConnectionState(from: result)
completion(result)
}
}
return
}
let (type, path1, path2, permissions) = parseCommand(command)
helper.executeCommand(type: type, path1: path1, path2: path2, permissions: permissions) { [weak self] result in
DispatchQueue.main.async {
self?.updateConnectionState(from: result)
completion(result)
}
}
} catch {
connectionState = .disconnected
completion("Error: \(error.localizedDescription)")
}
}
func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws {
let helper: HelperToolProtocol = try connectionQueue.sync {
if let existingConnection = connection,
let proxy = existingConnection.remoteObjectProxy as? HelperToolProtocol {
return proxy
}
guard let newConnection = createConnection() else {
throw HelperError.connectionFailed
}
connection = newConnection
guard let proxy = newConnection.remoteObjectProxy as? HelperToolProtocol else {
throw HelperError.proxyError
}
return proxy
}
let (type, path1, path2, permissions) = parseCommand(command)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<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) {
if result.starts(with: "Error:") {
connectionState = .disconnected
} else {
connectionState = .connected
}
}
private func parseCommand(_ command: String) -> (CommandType, String, String, Int) {
let components = command.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
if command.hasPrefix("installer -pkg") {
return (.install, components[2], "", 0)
} else if command.hasPrefix("rm -rf") {
let path = components.dropFirst(2).joined(separator: " ")
return (.uninstall, path, "", 0)
} else if command.hasPrefix("mv") || command.hasPrefix("cp") {
let paths = components.dropFirst(1)
let sourcePath = String(paths.first ?? "")
let destPath = paths.dropFirst().joined(separator: " ")
return (.moveFile, sourcePath, destPath, 0)
} else if command.hasPrefix("chmod") {
return (.setPermissions,
components.dropFirst(2).joined(separator: " "),
"",
Int(components[1]) ?? 0)
}
return (.shellCommand, command, "", 0)
}
private func needsUpdate() async -> Bool {
guard let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") else {
return true
}
return currentBuild != installedBuild
}
@objc private func handleConnectionInvalidation() {
DispatchQueue.main.async { [weak self] in
self?.connectionState = .disconnected
self?.connection?.invalidate()
self?.connection = nil
}
}
private func hasLegacyInstallation() -> Bool {
let legacyPlistPath = "/Library/LaunchDaemons/\(Self.helperIdentifier).plist"
let legacyHelperPath = "/Library/PrivilegedHelperTools/\(Self.helperIdentifier)"
return FileManager.default.fileExists(atPath: legacyPlistPath) ||
FileManager.default.fileExists(atPath: legacyHelperPath)
}
private func handleLegacyInstallation() {
logger.info("检测到旧的 SMJobBless 安装,开始清理")
let alert = NSAlert()
alert.messageText = String(localized: "检测到旧版本的 Helper")
alert.informativeText = String(localized: "系统检测到旧版本的 Adobe Downloader Helper需要升级到新版本。这将需要管理员权限来清理旧安装。")
alert.addButton(withTitle: String(localized: "升级"))
alert.addButton(withTitle: String(localized: "取消"))
if alert.runModal() == .alertFirstButtonReturn {
Task {
do {
try await cleanupLegacyInstallation()
registerHelper()
} catch {
logger.error("清理旧安装失败: \(error)")
showError("清理旧版本失败: \(error.localizedDescription)")
}
}
}
}
private func cleanupLegacyInstallation() async throws {
let script = """
#!/bin/bash
sudo /bin/launchctl unload /Library/LaunchDaemons/\(Self.helperIdentifier).plist 2>/dev/null
sudo /bin/rm -f /Library/LaunchDaemons/\(Self.helperIdentifier).plist
sudo /bin/rm -f /Library/PrivilegedHelperTools/\(Self.helperIdentifier)
sudo /usr/bin/killall -u root -9 \(Self.helperIdentifier) 2>/dev/null || true
exit 0
"""
let tempDir = FileManager.default.temporaryDirectory
let scriptURL = tempDir.appendingPathComponent("cleanup_legacy_helper.sh")
try script.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path)
return try await withCheckedThrowingContinuation { continuation in
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
task.arguments = ["-e", "do shell script \"\(scriptURL.path)\" with administrator privileges"]
do {
try task.run()
task.waitUntilExit()
try? FileManager.default.removeItem(at: scriptURL)
if task.terminationStatus == 0 {
logger.info("旧安装清理成功")
continuation.resume()
} else {
continuation.resume(throwing: HelperError.installationFailed("清理脚本执行失败"))
}
} catch {
try? FileManager.default.removeItem(at: scriptURL)
continuation.resume(throwing: error)
}
}
}
private func showApprovalGuidance() {
let alert = NSAlert()
alert.messageText = String(localized: "需要在系统设置中允许 Helper")
alert.informativeText = String(localized: "Adobe Downloader 需要通过后台服务来安装与移动文件。请在\"系统设置 → 通用 → 登录项与扩展\"中允许此应用的后台项目。")
alert.addButton(withTitle: String(localized: "打开系统设置"))
alert.addButton(withTitle: String(localized: "稍后设置"))
if alert.runModal() == .alertFirstButtonReturn {
SMAppService.openSystemSettingsLoginItems()
}
}
private func handleRegistrationError(_ error: Error) {
logger.error("Helper 注册错误: \(error)")
let nsError = error as NSError
let message = String(localized: "Helper 注册失败")
var informative = error.localizedDescription
if nsError.domain == "com.apple.ServiceManagement.SMAppServiceError" {
switch nsError.code {
case 1: // kSMAppServiceErrorDomain
informative = String(localized: "Helper 文件不存在或损坏,请重新安装应用")
case 2: // Permission denied
informative = String(localized: "权限被拒绝,请检查应用签名和权限设置")
default:
break
}
}
showError(message, informative: informative)
}
private func showError(_ message: String, informative: String? = nil) {
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = message
if let informative = informative {
alert.informativeText = informative
}
alert.alertStyle = .warning
alert.addButton(withTitle: String(localized: "确定"))
alert.runModal()
}
}
}
fileprivate extension Notification.Name {
static let NSXPCConnectionInvalid = Notification.Name("NSXPCConnectionInvalidNotification")
}

View File

@@ -0,0 +1,199 @@
//
// PrivilegedHelperAdapter.swift
// Adobe Downloader
//
// Created by X1a0He on 2025/07/20.
//
import Foundation
import AppKit
import ServiceManagement
@objcMembers
class PrivilegedHelperAdapter: NSObject, ObservableObject {
static let shared = PrivilegedHelperAdapter()
static let machServiceName = "com.x1a0he.macOS.Adobe-Downloader.helper"
@Published var connectionState: ConnectionState = .disconnected
private let modernManager: ModernPrivilegedHelperManager
var connectionSuccessBlock: (() -> Void)?
enum HelperStatus {
case installed
case noFound
case needUpdate
}
enum ConnectionState {
case connected
case disconnected
case connecting
var description: String {
switch self {
case .connected:
return String(localized: "已连接")
case .disconnected:
return String(localized: "未连接")
case .connecting:
return String(localized: "正在连接")
}
}
}
override init() {
self.modernManager = ModernPrivilegedHelperManager.shared
super.init()
modernManager.$connectionState
.receive(on: DispatchQueue.main)
.sink { [weak self] modernState in
self?.connectionState = self?.convertConnectionState(modernState) ?? .disconnected
}
.store(in: &cancellables)
Task {
await initializeWithMigration()
}
}
private var cancellables = Set<AnyCancellable>()
func checkInstall() {
Task {
await modernManager.checkAndInstallHelper()
}
}
func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) {
Task {
let modernStatus = await modernManager.getHelperStatus()
let legacyStatus = convertHelperStatus(modernStatus)
await MainActor.run {
callback(legacyStatus)
}
}
}
static var getHelperStatus: Bool {
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + machServiceName)
guard CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else { return false }
let appService = SMAppService.daemon(plistName: machServiceName)
return appService.status == .enabled
}
func executeCommand(_ command: String, completion: @escaping (String) -> Void) {
modernManager.executeCommand(command, completion: completion)
}
func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws {
try await modernManager.executeInstallation(command, progress: progress)
}
func reconnectHelper(completion: @escaping (Bool, String) -> Void) {
Task {
do {
try await modernManager.reconnectHelper()
completion(true, String(localized: "重新连接成功"))
} catch {
completion(false, error.localizedDescription)
}
}
}
func reinstallHelper(completion: @escaping (Bool, String) -> Void) {
Task {
do {
try await modernManager.uninstallHelper()
await modernManager.checkAndInstallHelper()
try await Task.sleep(nanoseconds: 2_000_000_000)
let status = await modernManager.getHelperStatus()
switch status {
case .installed:
completion(true, String(localized: "重新安装成功"))
case .needsApproval:
completion(false, String(localized: "需要在系统设置中批准"))
default:
completion(false, String(localized: "重新安装失败"))
}
} catch {
completion(false, error.localizedDescription)
}
}
}
func removeInstallHelper(completion: ((Bool) -> Void)? = nil) {
Task {
do {
try await modernManager.uninstallHelper()
completion?(true)
} catch {
completion?(false)
}
}
}
func forceReinstallHelper() {
reinstallHelper { _, _ in }
}
func disconnectHelper() {
Task {
await modernManager.disconnectHelper()
}
}
func uninstallHelperViaTerminal(completion: @escaping (Bool, String) -> Void) {
Task {
do {
try await modernManager.uninstallHelper()
completion(true, String(localized: "Helper 已完全卸载"))
} catch {
completion(false, error.localizedDescription)
}
}
}
public func getHelperProxy() throws -> HelperToolProtocol {
return try modernManager.getHelperProxy()
}
private func initializeWithMigration() async {
do {
let _ = try await ModernPrivilegedHelperManager.initializeWithMigration()
connectionSuccessBlock?()
} catch {
print("Helper 初始化失败: \(error)")
}
}
private func convertConnectionState(_ modernState: ModernPrivilegedHelperManager.ConnectionState) -> ConnectionState {
switch modernState {
case .connected:
return .connected
case .disconnected:
return .disconnected
case .connecting:
return .connecting
}
}
private func convertHelperStatus(_ modernStatus: ModernPrivilegedHelperManager.HelperStatus) -> HelperStatus {
switch modernStatus {
case .installed:
return .installed
case .notInstalled, .needsApproval, .legacy:
return .noFound
case .requiresUpdate:
return .needUpdate
}
}
}
import Combine

View File

@@ -1,757 +0,0 @@
//
// Untitled.swift
// Adobe Downloader
//
// Created by X1a0He on 11/12/24.
//
import AppKit
import Cocoa
import ServiceManagement
@objc enum CommandType: Int {
case install
case uninstall
case moveFile
case setPermissions
case shellCommand
}
@objc protocol HelperToolProtocol {
@objc(executeCommand:path1:path2:permissions:withReply:)
func executeCommand(type: CommandType, path1: String, path2: String, permissions: Int, withReply reply: @escaping (String) -> Void)
func getInstallationOutput(withReply reply: @escaping (String) -> Void)
}
@objcMembers
class PrivilegedHelperManager: NSObject {
enum HelperStatus {
case installed
case noFound
case needUpdate
}
static let shared = PrivilegedHelperManager()
static let machServiceName = "com.x1a0he.macOS.Adobe-Downloader.helper"
var connectionSuccessBlock: (() -> Void)?
private var useLegacyInstall = false
private var connection: NSXPCConnection?
private var isInitializing = false
private var shouldAutoReconnect = true
@Published private(set) var connectionState: ConnectionState = .disconnected {
didSet {
if oldValue != connectionState {
if connectionState == .disconnected {
connection?.invalidate()
connection = nil
}
}
}
}
enum ConnectionState {
case connected
case disconnected
case connecting
var description: String {
switch self {
case .connected:
return String(localized: "已连接")
case .disconnected:
return String(localized: "未连接")
case .connecting:
return String(localized: "正在连接")
}
}
}
private let connectionQueue = DispatchQueue(label: "com.x1a0he.helper.connection")
override init() {
super.init()
initAuthorizationRef()
setupAutoReconnect()
NotificationCenter.default.addObserver(self,
selector: #selector(handleConnectionInvalidation),
name: .NSXPCConnectionInvalid,
object: nil)
}
@objc private func handleConnectionInvalidation() {
DispatchQueue.main.async { [weak self] in
self?.connectionState = .disconnected
self?.connection?.invalidate()
self?.connection = nil
}
}
private func setupAutoReconnect() {
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
if self.connectionState == .disconnected && self.shouldAutoReconnect {
_ = self.connectToHelper()
}
}
}
func checkInstall() {
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild") {
if currentBuild != installedBuild {
notifyInstall()
return
}
}
getHelperStatus { [weak self] status in
guard let self = self else { return }
switch status {
case .noFound:
if #available(macOS 13, *) {
let url = URL(string: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist")!
let status = SMAppService.statusForLegacyPlist(at: url)
if status == .requiresApproval {
let alert = NSAlert()
let notice = String(localized: "Adobe Downloader 需要通过后台Daemon进程来安装与移动文件请在\"系统偏好设置->登录项->允许在后台 中\"允许当前App")
let addition = String(localized: "如果在设置里没找到当前App可以尝试重置守护程序")
alert.messageText = notice + "\n" + addition
alert.addButton(withTitle: "打开系统登录项设置")
alert.addButton(withTitle: "重置守护程序")
if alert.runModal() == .alertFirstButtonReturn {
SMAppService.openSystemSettingsLoginItems()
} else {
removeInstallHelper()
}
}
}
fallthrough
case .needUpdate:
if Thread.isMainThread {
self.notifyInstall()
} else {
DispatchQueue.main.async {
self.notifyInstall()
}
}
case .installed:
self.connectionSuccessBlock?()
}
}
}
private func initAuthorizationRef() {
var authRef: AuthorizationRef?
let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authRef)
if status != OSStatus(errAuthorizationSuccess) {
return
}
}
private func installHelperDaemon() -> DaemonInstallResult {
var authRef: AuthorizationRef?
var authStatus = AuthorizationCreate(nil, nil, [], &authRef)
guard authStatus == errAuthorizationSuccess else {
return .authorizationFail
}
var authItem = AuthorizationItem(name: (kSMRightBlessPrivilegedHelper as NSString).utf8String!, valueLength: 0, value: nil, flags: 0)
var authRights = withUnsafeMutablePointer(to: &authItem) { pointer in
AuthorizationRights(count: 1, items: pointer)
}
let flags: AuthorizationFlags = [[], .interactionAllowed, .extendRights, .preAuthorize]
authStatus = AuthorizationCreate(&authRights, nil, flags, &authRef)
defer {
if let ref = authRef {
AuthorizationFree(ref, [])
}
}
guard authStatus == errAuthorizationSuccess else {
return .getAdminFail
}
var error: Unmanaged<CFError>?
if SMJobBless(kSMDomainSystemLaunchd, PrivilegedHelperManager.machServiceName as CFString, authRef, &error) == false {
if let blessError = error?.takeRetainedValue() {
let nsError = blessError as Error as NSError
NSAlert.alert(with: "SMJobBless failed with error: \(blessError)\nError domain: \(nsError.domain)\nError code: \(nsError.code)\nError description: \(nsError.localizedDescription)\nError user info: \(nsError.userInfo)")
return .blessError(nsError.code)
}
return .blessError(-1)
}
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
UserDefaults.standard.set(currentBuild, forKey: "InstalledHelperBuild")
}
return .success
}
func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) {
var called = false
let reply: ((HelperStatus) -> Void) = {
status in
if called { return }
called = true
callback(status)
}
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName)
guard
CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else {
reply(.noFound)
return
}
let helperFileExists = FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)")
if !helperFileExists {
reply(.noFound)
return
}
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild"),
currentBuild != installedBuild {
reply(.needUpdate)
return
}
reply(.installed)
}
static var getHelperStatus: Bool {
if let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
let installedBuild = UserDefaults.standard.string(forKey: "InstalledHelperBuild"),
currentBuild != installedBuild {
return false
}
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + machServiceName)
guard CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) != nil else { return false }
return FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(machServiceName)")
}
func reinstallHelper(completion: @escaping (Bool, String) -> Void) {
shouldAutoReconnect = false
uninstallHelperViaTerminal { [weak self] success, message in
guard let self = self else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let result = self.installHelperDaemon()
switch result {
case .success:
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
guard let self = self else { return }
self.shouldAutoReconnect = true
self.tryConnect(retryCount: 3, delay: 1, completion: completion)
}
case .authorizationFail:
self.shouldAutoReconnect = true
completion(false, String(localized: "获取授权失败"))
case .getAdminFail:
self.shouldAutoReconnect = true
completion(false, String(localized: "获取管理员权限失败"))
case .blessError(_):
self.shouldAutoReconnect = true
completion(false, String(localized: "安装失败: \(result.alertContent)"))
}
}
}
}
private func tryConnect(retryCount: Int, delay: TimeInterval = 2.0, completion: @escaping (Bool, String) -> Void) {
struct Static {
static var currentAttempt = 0
}
if retryCount == 3 {
Static.currentAttempt = 0
}
Static.currentAttempt += 1
guard retryCount > 0 else {
completion(false, String(localized: "多次尝试连接失败"))
return
}
guard let connection = connectToHelper() else {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.tryConnect(retryCount: retryCount - 1, delay: delay * 1, completion: completion)
}
return
}
guard let helper = connection.remoteObjectProxy as? HelperToolProtocol else {
completion(false, String(localized: "无法获取Helper代理"))
return
}
helper.executeCommand(type: .shellCommand, path1: "id -u", path2: "", permissions: 0) { result in
if result == "0" || result.contains("0") {
completion(true, String(localized: "Helper 重新安装成功"))
} else {
print("Helper验证失败返回结果: \(result)")
completion(false, String(localized: "Helper 安装失败: \(result)"))
}
}
}
func removeInstallHelper(completion: ((Bool) -> Void)? = nil) {
if FileManager.default.fileExists(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist") {
try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist")
}
if FileManager.default.fileExists(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") {
try? FileManager.default.removeItem(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)")
}
completion?(true)
}
func connectToHelper() -> NSXPCConnection? {
return connectionQueue.sync {
return createConnection()
}
}
private func createConnection() -> NSXPCConnection? {
DispatchQueue.main.async {
self.connectionState = .connecting
}
if let existingConnection = connection {
existingConnection.invalidate()
connection = nil
}
let newConnection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName,
options: .privileged)
let interface = NSXPCInterface(with: HelperToolProtocol.self)
interface.setClasses(NSSet(array: [NSString.self, NSNumber.self]) as! Set<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: "id -u", path2: "", permissions: 0) { [weak self] result in
if result.contains("0") || result == "0" {
isConnected = true
DispatchQueue.main.async {
self?.connection = newConnection
self?.connectionState = .connected
}
}
semaphore.signal()
}
_ = semaphore.wait(timeout: .now() + 1.0)
}
if !isConnected {
newConnection.invalidate()
DispatchQueue.main.async {
self.connectionState = .disconnected
}
return nil
}
return newConnection
}
func executeCommand(_ command: String, completion: @escaping (String) -> Void) {
do {
let helper = try getHelperProxy()
if command.contains("perl") || command.contains("codesign") || command.contains("xattr") {
helper.executeCommand(type: .shellCommand, path1: command, path2: "", permissions: 0) { [weak self] result in
DispatchQueue.main.async {
if result.starts(with: "Error:") {
self?.connectionState = .disconnected
} else {
self?.connectionState = .connected
}
completion(result)
}
}
return
}
let (type, path1, path2, permissions) = parseCommand(command)
helper.executeCommand(type: type, path1: path1, path2: path2, permissions: permissions) { [weak self] result in
DispatchQueue.main.async {
if result.starts(with: "Error:") {
self?.connectionState = .disconnected
} else {
self?.connectionState = .connected
}
completion(result)
}
}
} catch {
connectionState = .disconnected
completion("Error: \(error.localizedDescription)")
}
}
private func parseCommand(_ command: String) -> (CommandType, String, String, Int) {
let components = command.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
if command.hasPrefix("installer -pkg") {
return (.install, components[2], "", 0)
} else if command.hasPrefix("rm -rf") {
let path = components.dropFirst(2).joined(separator: " ")
return (.uninstall, path, "", 0)
} else if command.hasPrefix("mv") || command.hasPrefix("cp") {
let paths = components.dropFirst(1)
let sourcePath = String(paths.first ?? "")
let destPath = paths.dropFirst().joined(separator: " ")
return (.moveFile, sourcePath, destPath, 0)
} else if command.hasPrefix("chmod") {
return (.setPermissions,
components.dropFirst(2).joined(separator: " "),
"",
Int(components[1]) ?? 0)
}
return (.shellCommand, command, "", 0)
}
func reconnectHelper(completion: @escaping (Bool, String) -> Void) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.connectionState = .disconnected
self.connection?.invalidate()
self.connection = nil
self.shouldAutoReconnect = true
DispatchQueue.main.asyncAfter(deadline: .now()) {
do {
let helper = try self.getHelperProxy()
helper.executeCommand(type: .install, path1: "id -u", path2: "", permissions: 0) { result in
DispatchQueue.main.async {
if result.contains("0") || result == "0" {
self.connectionState = .connected
completion(true, String(localized: "Helper 重新连接成功"))
} else {
self.connectionState = .disconnected
completion(false, String(localized: "Helper 响应异常: \(result)"))
}
}
}
} catch HelperError.connectionFailed {
completion(false, String(localized: "无法连接到 Helper"))
} catch HelperError.proxyError {
completion(false, String(localized: "无法获取 Helper 代理"))
} catch {
completion(false, String(localized: "连接出现错误: \(error.localizedDescription)"))
}
}
}
}
func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws {
let helper: HelperToolProtocol = try connectionQueue.sync {
if let existingConnection = connection,
let proxy = existingConnection.remoteObjectProxy as? HelperToolProtocol {
return proxy
}
guard let newConnection = createConnection() else {
throw HelperError.connectionFailed
}
connection = newConnection
guard let proxy = newConnection.remoteObjectProxy as? HelperToolProtocol else {
throw HelperError.proxyError
}
return proxy
}
let (type, path1, path2, permissions) = parseCommand(command)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<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 forceReinstallHelper() {
guard !isInitializing else { return }
isInitializing = true
shouldAutoReconnect = false
uninstallHelperViaTerminal { [weak self] success, _ in
guard let self = self else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.notifyInstall()
self.isInitializing = false
self.shouldAutoReconnect = true
}
}
}
func disconnectHelper() {
connectionQueue.sync {
shouldAutoReconnect = false
if let existingConnection = connection {
existingConnection.invalidate()
}
connection = nil
connectionState = .disconnected
}
}
func uninstallHelperViaTerminal(completion: @escaping (Bool, String) -> Void) {
disconnectHelper()
let script = """
#!/bin/bash
sudo /bin/launchctl unload /Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist
sudo /bin/rm -f /Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist
sudo /bin/rm -f /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)
sudo /usr/bin/killall -u root -9 \(PrivilegedHelperManager.machServiceName)
exit 0
"""
let tempDir = FileManager.default.temporaryDirectory
let scriptURL = tempDir.appendingPathComponent("uninstall_helper.sh")
do {
try script.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path)
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
task.arguments = ["-e", "do shell script \"\(scriptURL.path)\" with administrator privileges"]
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
task.waitUntilExit()
if task.terminationStatus == 0 {
UserDefaults.standard.removeObject(forKey: "InstalledHelperBuild")
connectionState = .disconnected
connection = nil
completion(true, String(localized: "Helper 已完全卸载"))
} else {
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let errorString = String(data: errorData, encoding: .utf8) ?? "未知错误"
completion(false, String(localized: "卸载失败: \(errorString)"))
}
self.shouldAutoReconnect = true
} catch {
self.shouldAutoReconnect = true
completion(false, String(localized: "执行卸载脚本失败: \(error.localizedDescription)"))
}
try? FileManager.default.removeItem(at: scriptURL)
} catch {
self.shouldAutoReconnect = true
completion(false, String(localized: "准备卸载脚本失败: \(error.localizedDescription)"))
}
}
}
extension PrivilegedHelperManager {
private func notifyInstall() {
guard !isInitializing else { return }
shouldAutoReconnect = false
let result = installHelperDaemon()
if case .success = result {
shouldAutoReconnect = true
checkInstall()
return
}
result.alertAction()
let ret = result.shouldRetryLegacyWay()
useLegacyInstall = ret.0
let isCancle = ret.1
if !isCancle, useLegacyInstall {
shouldAutoReconnect = true
checkInstall()
} else if isCancle, !useLegacyInstall {
shouldAutoReconnect = true
NSAlert.alert(with: String(localized: "获取管理员授权失败,用户主动取消授权!"))
} else {
shouldAutoReconnect = true
}
}
}
private enum DaemonInstallResult {
case success
case authorizationFail
case getAdminFail
case blessError(Int)
var alertContent: String {
switch self {
case .success:
return ""
case .authorizationFail: return "Failed to create authorization!"
case .getAdminFail: return "The user actively cancels the authorization, Failed to get admin authorization! "
case let .blessError(code):
switch code {
case kSMErrorInternalFailure: return "blessError: kSMErrorInternalFailure"
case kSMErrorInvalidSignature: return "blessError: kSMErrorInvalidSignature"
case kSMErrorAuthorizationFailure: return "blessError: kSMErrorAuthorizationFailure"
case kSMErrorToolNotValid: return "blessError: kSMErrorToolNotValid"
case kSMErrorJobNotFound: return "blessError: kSMErrorJobNotFound"
case kSMErrorServiceUnavailable: return "blessError: kSMErrorServiceUnavailable"
case kSMErrorJobMustBeEnabled: return "Adobe Downloader Helper is disabled by other process. Please run \"sudo launchctl enable system/\(PrivilegedHelperManager.machServiceName)\" in your terminal. The command has been copied to your pasteboard"
case kSMErrorInvalidPlist: return "blessError: kSMErrorInvalidPlist"
default:
return "bless unknown error:\(code)"
}
}
}
func shouldRetryLegacyWay() -> (Bool, Bool) {
switch self {
case .success: return (false, false)
case let .blessError(code):
switch code {
case kSMErrorJobMustBeEnabled:
return (false, false)
default:
return (true, false)
}
case .authorizationFail:
return (true, false)
case .getAdminFail:
return (false, true)
}
}
func alertAction() {
switch self {
case let .blessError(code):
switch code {
case kSMErrorJobMustBeEnabled:
NSPasteboard.general.clearContents()
NSPasteboard.general.setString("sudo launchctl enable system/\(PrivilegedHelperManager.machServiceName)", forType: .string)
default:
break
}
default:
break
}
}
}
extension NSAlert {
static func alert(with text: String) {
let alert = NSAlert()
alert.messageText = text
alert.alertStyle = .warning
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.runModal()
}
}
enum HelperError: LocalizedError {
case connectionFailed
case proxyError
case authorizationFailed
case installationFailed(String)
var errorDescription: String? {
switch self {
case .connectionFailed:
return String(localized: "无法连接到 Helper")
case .proxyError:
return String(localized: "无法获取 Helper 代理")
case .authorizationFailed:
return String(localized: "获取授权失败")
case .installationFailed(let reason):
return String(localized: "安装失败: \(reason)")
}
}
}
extension PrivilegedHelperManager {
public func getHelperProxy() throws -> HelperToolProtocol {
if connectionState != .connected {
guard let newConnection = connectToHelper() else {
throw HelperError.connectionFailed
}
connection = newConnection
}
guard let helper = connection?.remoteObjectProxyWithErrorHandler({ [weak self] error in
self?.connectionState = .disconnected
}) as? HelperToolProtocol else {
throw HelperError.proxyError
}
return helper
}
}
extension Notification.Name {
static let NSXPCConnectionInvalid = Notification.Name("NSXPCConnectionInvalidNotification")
}

View File

@@ -11,8 +11,8 @@
</dict>
<key>SMPrivilegedExecutables</key>
<dict>
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<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>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<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>
</dict>
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml</string>

View File

@@ -37,7 +37,7 @@ actor InstallManager {
private func terminateSetupProcesses() async {
let _ = await withCheckedContinuation { continuation in
PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { result in
ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { result in
continuation.resume(returning: result)
}
}
@@ -155,7 +155,7 @@ actor InstallManager {
for logFile in logFiles {
let removeCommand = "rm -f '\(logFile)'"
let result = await withCheckedContinuation { continuation in
PrivilegedHelperManager.shared.executeCommand(removeCommand) { result in
ModernPrivilegedHelperManager.shared.executeCommand(removeCommand) { result in
continuation.resume(returning: result)
}
}
@@ -174,7 +174,7 @@ actor InstallManager {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
Task.detached {
do {
try await PrivilegedHelperManager.shared.executeInstallation(installCommand) { output in
try await ModernPrivilegedHelperManager.shared.executeInstallation(installCommand) { output in
Task { @MainActor in
if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression),
let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
@@ -182,7 +182,7 @@ actor InstallManager {
if exitCode == 0 {
progressHandler(1.0, String(localized: "安装完成"))
PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in }
ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in }
continuation.resume()
return
} else {
@@ -241,7 +241,7 @@ actor InstallManager {
}
func cancel() {
PrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in }
ModernPrivilegedHelperManager.shared.executeCommand("pkill -f Setup") { _ in }
}
func getInstallCommand(for driverPath: String) -> String {

View File

@@ -84,14 +84,14 @@ class ModifySetup {
if isSetupBackup() {
print("检测到备份文件,尝试从备份恢复...")
PrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(backupPath)' '\(setupPath)'") { result in
ModernPrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(backupPath)' '\(setupPath)'") { result in
if result.starts(with: "Error:") {
print("从备份恢复失败: \(result)")
}
completion(!result.starts(with: "Error:"))
}
} else {
PrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(setupPath)' '\(backupPath)'") { result in
ModernPrivilegedHelperManager.shared.executeCommand("/bin/cp -f '\(setupPath)' '\(backupPath)'") { result in
if result.starts(with: "Error:") {
print("创建备份失败: \(result)")
completion(false)
@@ -100,7 +100,7 @@ class ModifySetup {
if !result.starts(with: "Error:") {
if FileManager.default.fileExists(atPath: backupPath) {
PrivilegedHelperManager.shared.executeCommand("/bin/chmod 644 '\(backupPath)'") { chmodResult in
ModernPrivilegedHelperManager.shared.executeCommand("/bin/chmod 644 '\(backupPath)'") { chmodResult in
if chmodResult.starts(with: "Error:") {
print("设置备份文件权限失败: \(chmodResult)")
}
@@ -138,7 +138,7 @@ class ModifySetup {
return
}
PrivilegedHelperManager.shared.executeCommand(commands[index]) { result in
ModernPrivilegedHelperManager.shared.executeCommand(commands[index]) { result in
if result.starts(with: "Error:") {
print("命令执行失败: \(commands[index])")
print("错误信息: \(result)")

View File

@@ -1364,7 +1364,7 @@ class NewDownloadUtils {
private func executePrivilegedCommand(_ command: String) async -> String {
return await withCheckedContinuation { continuation in
PrivilegedHelperManager.shared.executeCommand(command) { result in
ModernPrivilegedHelperManager.shared.executeCommand(command) { result in
if result.starts(with: "Error:") {
print("命令执行失败: \(command)")
print("错误信息: \(result)")

View File

@@ -22,7 +22,6 @@ private enum AboutViewConstants {
static let links: [(title: String, url: String)] = [
("@X1a0He", "https://t.me/X1a0He_bot"),
("Github: Adobe Downloader", "https://github.com/X1a0He/Adobe-Downloader"),
("QiuChenly: InjectLib", "https://github.com/QiuChenly/InjectLib")
]
}
@@ -239,7 +238,7 @@ final class GeneralSettingsViewModel: ObservableObject {
self.helperConnectionStatus = .connecting
PrivilegedHelperManager.shared.$connectionState
ModernPrivilegedHelperManager.shared.$connectionState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
switch state {
@@ -819,6 +818,7 @@ struct HelperStatusRow: View {
@Binding var helperAlertMessage: String
@Binding var helperAlertSuccess: Bool
@State private var isReinstallingHelper = false
@State private var helperStatus: ModernPrivilegedHelperManager.HelperStatus = .notInstalled
var body: some View {
VStack(alignment: .leading, spacing: 8) {
@@ -826,7 +826,7 @@ struct HelperStatusRow: View {
Text("安装状态: ")
.font(.system(size: 14, weight: .medium))
if PrivilegedHelperManager.getHelperStatus {
if helperStatus == .installed {
HStack(spacing: 5) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
@@ -863,12 +863,25 @@ struct HelperStatusRow: View {
Button(action: {
isReinstallingHelper = true
PrivilegedHelperManager.shared.removeInstallHelper()
PrivilegedHelperManager.shared.reinstallHelper { success, message in
helperAlertSuccess = success
helperAlertMessage = message
showHelperAlert = true
isReinstallingHelper = false
Task {
do {
try await ModernPrivilegedHelperManager.shared.uninstallHelper()
await ModernPrivilegedHelperManager.shared.checkAndInstallHelper()
await MainActor.run {
helperAlertSuccess = true
helperAlertMessage = "Helper 重新安装成功"
showHelperAlert = true
isReinstallingHelper = false
}
} catch {
await MainActor.run {
helperAlertSuccess = false
helperAlertMessage = error.localizedDescription
showHelperAlert = true
isReinstallingHelper = false
}
}
}
}) {
HStack(spacing: 4) {
@@ -885,7 +898,7 @@ struct HelperStatusRow: View {
.help("完全卸载并重新安装 Helper")
}
if !PrivilegedHelperManager.getHelperStatus {
if helperStatus != .installed {
Text("Helper 未安装将导致无法执行需要管理员权限的操作")
.font(.caption)
.foregroundColor(.red)
@@ -914,12 +927,23 @@ struct HelperStatusRow: View {
Spacer()
Button(action: {
if PrivilegedHelperManager.getHelperStatus &&
if helperStatus == .installed &&
viewModel.helperConnectionStatus != .connected {
PrivilegedHelperManager.shared.reconnectHelper { success, message in
helperAlertSuccess = success
helperAlertMessage = message
showHelperAlert = true
Task {
do {
try await ModernPrivilegedHelperManager.shared.reconnectHelper()
await MainActor.run {
helperAlertSuccess = true
helperAlertMessage = "重新连接成功"
showHelperAlert = true
}
} catch {
await MainActor.run {
helperAlertSuccess = false
helperAlertMessage = error.localizedDescription
showHelperAlert = true
}
}
}
}
}) {
@@ -937,6 +961,9 @@ struct HelperStatusRow: View {
.help("尝试重新连接到已安装的 Helper")
}
}
.task {
helperStatus = await ModernPrivilegedHelperManager.shared.getHelperStatus()
}
}
private var helperStatusColor: Color {
@@ -949,7 +976,7 @@ struct HelperStatusRow: View {
}
private var shouldDisableReconnectButton: Bool {
return !PrivilegedHelperManager.getHelperStatus ||
return helperStatus != .installed ||
viewModel.helperConnectionStatus == .connected ||
isReinstallingHelper
}

View File

@@ -11,6 +11,7 @@ struct CleanConfigView: View {
@State private var showAlert = false
@State private var alertMessage = ""
@State private var chipInfo: String = ""
@State private var helperStatus: ModernPrivilegedHelperManager.HelperStatus = .notInstalled
private func getChipInfo() -> String {
var size = 0
@@ -83,6 +84,9 @@ struct CleanConfigView: View {
.onAppear {
chipInfo = getChipInfo()
}
.task {
helperStatus = await ModernPrivilegedHelperManager.shared.getHelperStatus()
}
}
private func cleanConfig() {
@@ -103,8 +107,8 @@ struct CleanConfigView: View {
try FileManager.default.setAttributes([.posixPermissions: 0o755],
ofItemAtPath: scriptURL.path)
if PrivilegedHelperManager.getHelperStatus {
PrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in
if helperStatus == .installed {
ModernPrivilegedHelperManager.shared.executeCommand("open -a Terminal \(scriptURL.path)") { output in
if output.starts(with: "Error") {
alertMessage = "清空配置失败: \(output)"
showAlert = true

View File

@@ -338,7 +338,7 @@ struct CleanupView: View {
}
timeoutTimer.resume()
PrivilegedHelperManager.shared.executeCommand(command) { [self] output in
ModernPrivilegedHelperManager.shared.executeCommand(command) { [self] (output: String) in
timeoutTimer.cancel()
DispatchQueue.main.async {
if let index = cleanupLogs.lastIndex(where: { $0.command == command }) {
@@ -642,4 +642,4 @@ struct LogContentView: View {
}
}
}
}
}

View File

@@ -128,7 +128,7 @@ struct DownloadProgressView: View {
#if DEBUG
Button(action: {
do {
_ = try PrivilegedHelperManager.shared.getHelperProxy()
_ = try ModernPrivilegedHelperManager.shared.getHelperProxy()
showInstallPrompt = false
isInstalling = true
Task {
@@ -158,7 +158,7 @@ struct DownloadProgressView: View {
if ModifySetup.isSetupModified() {
Button(action: {
do {
_ = try PrivilegedHelperManager.shared.getHelperProxy()
_ = try ModernPrivilegedHelperManager.shared.getHelperProxy()
showInstallPrompt = false
isInstalling = true
Task {

View File

@@ -5,7 +5,11 @@
<key>CFBundleIdentifier</key>
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
<key>CFBundleName</key>
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
<string>Adobe Downloader Helper</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>SMAuthorizedClients</key>
<array>
<string>identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he@outlook.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */</string>

View File

@@ -9,12 +9,8 @@
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<true/>
</dict>
<key>Program</key>
<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>BundleProgram</key>
<string>Contents/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.x1a0he.macOS.Adobe-Downloader.helper</string>
<key>MachServices</key>
<dict>
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<true/>
</dict>
<key>BundleProgram</key>
<string>Contents/Library/LaunchDaemons/com.x1a0he.macOS.Adobe-Downloader.helper</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/tmp/com.x1a0he.macOS.Adobe-Downloader.helper.err</string>
<key>StandardOutPath</key>
<string>/tmp/com.x1a0he.macOS.Adobe-Downloader.helper.out</string>
</dict>
</plist>