From 95a534c0071d126d4d78ce77245e87a864f5e8af Mon Sep 17 00:00:00 2001 From: X1a0He Date: Wed, 13 Nov 2024 13:20:25 +0800 Subject: [PATCH] feat: Add Helper status monitoring. --- Adobe Downloader.xcodeproj/project.pbxproj | 8 +- .../xcschemes/Adobe Downloader.xcscheme | 2 +- Adobe Downloader/Adobe DownloaderApp.swift | 5 + Adobe Downloader/Commons/Structs.swift | 5 +- Adobe Downloader/ContentView.swift | 20 +- .../PrivilegedHelperManager.swift | 200 +++++++++++++++++- .../PrivilegedHelperManagerLegacy.swift | 95 --------- Adobe Downloader/Info.plist | 4 +- Adobe Downloader/NetworkManager.swift | 55 ++++- Adobe Downloader/Views/AboutView.swift | 153 ++++++++++++++ Adobe Downloader/Views/SettingsView.swift | 83 -------- .../AdobeDownloaderHelperTool.entitlements | 15 +- AdobeDownloaderHelperTool/Info.plist | 7 +- AdobeDownloaderHelperTool/Launchd.plist | 16 +- AdobeDownloaderHelperTool/main.swift | 85 +++++++- Localizables/Localizable.xcstrings | 41 ++++ update-log.md | 7 + 17 files changed, 597 insertions(+), 204 deletions(-) delete mode 100644 Adobe Downloader/HelperManager/PrivilegedHelperManagerLegacy.swift delete mode 100644 Adobe Downloader/Views/SettingsView.swift diff --git a/Adobe Downloader.xcodeproj/project.pbxproj b/Adobe Downloader.xcodeproj/project.pbxproj index b61a209..8c6b63a 100644 --- a/Adobe Downloader.xcodeproj/project.pbxproj +++ b/Adobe Downloader.xcodeproj/project.pbxproj @@ -232,7 +232,7 @@ CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements; CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = NO; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TG862GVKHK; ENABLE_HARDENED_RUNTIME = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist; @@ -261,7 +261,7 @@ CODE_SIGN_ENTITLEMENTS = AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements; CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = NO; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TG862GVKHK; ENABLE_HARDENED_RUNTIME = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = AdobeDownloaderHelperTool/Info.plist; @@ -418,7 +418,7 @@ CURRENT_PROJECT_VERSION = 120; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TG862GVKHK; ENABLE_HARDENED_RUNTIME = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -450,7 +450,7 @@ CURRENT_PROJECT_VERSION = 120; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Adobe Downloader/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TG862GVKHK; ENABLE_HARDENED_RUNTIME = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme b/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme index 4ef56ca..76e985d 100644 --- a/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme +++ b/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme @@ -31,7 +31,7 @@ shouldAutocreateTestPlan = "YES"> Void) +} + @objcMembers class PrivilegedHelperManager: NSObject { @@ -23,10 +27,23 @@ class PrivilegedHelperManager: NSObject { var connectionSuccessBlock: (() -> Void)? private var useLegacyInstall = false + private var connection: NSXPCConnection? + + @Published private(set) var connectionState: ConnectionState = .disconnected + + enum ConnectionState { + case connected + case disconnected + case connecting + } override init() { super.init() initAuthorizationRef() + + DispatchQueue.main.async { [weak self] in + _ = self?.connectToHelper() + } } func checkInstall() { @@ -47,7 +64,7 @@ class PrivilegedHelperManager: NSObject { if alert.runModal() == .alertFirstButtonReturn { SMAppService.openSystemSettingsLoginItems() } else { - self.removeInstallHelper() + removeInstallHelper() } } } @@ -74,11 +91,7 @@ class PrivilegedHelperManager: NSObject { } } - private func installHelperDaemon() -> DaemonInstallResult { - defer { - - } var authRef: AuthorizationRef? var authStatus = AuthorizationCreate(nil, nil, [], &authRef) @@ -102,9 +115,6 @@ class PrivilegedHelperManager: NSObject { } var error: Unmanaged? - let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/" + PrivilegedHelperManager.machServiceName) - - if SMJobBless(kSMDomainSystemLaunchd, PrivilegedHelperManager.machServiceName as CFString, authRef, &error) == false { if let blessError = error?.takeRetainedValue() { @@ -117,7 +127,7 @@ class PrivilegedHelperManager: NSObject { return .success } - private func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) { + func getHelperStatus(callback: @escaping ((HelperStatus) -> Void)) { var called = false let reply: ((HelperStatus) -> Void) = { status in @@ -142,13 +152,183 @@ class PrivilegedHelperManager: NSObject { reply(.installed) } + + static var getHelperStatus: Bool { + var status = false + let semaphore = DispatchSemaphore(value: 0) + + shared.getHelperStatus { helperStatus in + status = helperStatus == .installed + semaphore.signal() + } + + semaphore.wait() + return status + } + + func reinstallHelper(completion: @escaping (Bool, String) -> Void) { + removeInstallHelper() + let result = installHelperDaemon() + + switch result { + case .success: + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + guard let self = self else { return } + + guard let connection = connectToHelper() else { + completion(false, "无法连接到Helper") + return + } + + guard let helper = connection.remoteObjectProxy as? HelperToolProtocol else { + completion(false, "无法获取Helper代理") + return + } + + helper.executeCommand("whoami") { result in + if result.contains("root") { + completion(true, "Helper 重新安装成功") + } else { + completion(false, "Helper未能获取root权限") + } + } + } + + case .authorizationFail: + completion(false, "获取授权失败") + case .getAdminFail: + completion(false, "获取管理员权限失败") + case let .blessError(code): + completion(false, "安装失败: \(result.alertContent)") + } + } + + func removeInstallHelper() { + try? FileManager.default.removeItem(atPath: "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)") + try? FileManager.default.removeItem(atPath: "/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist") + } + + private func connectToHelper() -> NSXPCConnection? { + connectionState = .connecting + + objc_sync_enter(self) + defer { objc_sync_exit(self) } + + if let existingConnection = connection, + existingConnection.remoteObjectProxy != nil { + connectionState = .connected + return existingConnection + } + + connection?.invalidate() + connection = nil + + let newConnection = NSXPCConnection(machServiceName: PrivilegedHelperManager.machServiceName, + options: .privileged) + + newConnection.remoteObjectInterface = NSXPCInterface(with: HelperToolProtocol.self) + + newConnection.interruptionHandler = { [weak self] in + DispatchQueue.main.async { + self?.connectionState = .disconnected + self?.connection?.invalidate() + self?.connection = nil + } + } + + newConnection.invalidationHandler = { [weak self] in + DispatchQueue.main.async { + self?.connectionState = .disconnected + self?.connection?.invalidate() + self?.connection = nil + } + } + + newConnection.resume() + connection = newConnection + + if let helper = newConnection.remoteObjectProxy as? HelperToolProtocol { + helper.executeCommand("whoami") { [weak self] result in + if result == "root" { + DispatchQueue.main.async { + self?.connectionState = .connected + } + } else { + DispatchQueue.main.async { + self?.connectionState = .disconnected + } + } + } + } + + return newConnection + } + + func executeCommand(_ command: String, completion: @escaping (String) -> Void) { + guard let connection = connectToHelper() else { + connectionState = .disconnected + completion("Error: Could not connect to helper") + return + } + + guard let helper = connection.remoteObjectProxyWithErrorHandler({ error in + self.connectionState = .disconnected + }) as? HelperToolProtocol else { + connectionState = .disconnected + completion("Error: Could not get helper proxy") + return + } + + helper.executeCommand(command) { [weak self] result in + DispatchQueue.main.async { + if self?.connection == nil { + self?.connectionState = .disconnected + completion("Error: Connection lost") + return + } + + if result.starts(with: "Error:") { + self?.connectionState = .disconnected + } else { + self?.connectionState = .connected + } + + completion(result) + } + } + } + + func reconnectHelper(completion: @escaping (Bool, String) -> Void) { + connection?.invalidate() + connection = nil + + guard let newConnection = connectToHelper() else { + print("重新连接失败") + completion(false, "无法连接到 Helper") + return + } + + guard let helper = newConnection.remoteObjectProxyWithErrorHandler({ error in + completion(false, "连接出现错误: \(error.localizedDescription)") + }) as? HelperToolProtocol else { + completion(false, "无法获取 Helper 代理") + return + } + + helper.executeCommand("whoami") { result in + if result == "root" { + completion(true, "Helper 重新连接成功") + } else { + completion(false, "Helper 响应异常") + } + } + } } extension PrivilegedHelperManager { private func notifyInstall() { if useLegacyInstall { useLegacyInstall = false - legacyInstallHelper() checkInstall() return } diff --git a/Adobe Downloader/HelperManager/PrivilegedHelperManagerLegacy.swift b/Adobe Downloader/HelperManager/PrivilegedHelperManagerLegacy.swift deleted file mode 100644 index c93dcd0..0000000 --- a/Adobe Downloader/HelperManager/PrivilegedHelperManagerLegacy.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// PrivilegedHelperManagerLegacy.swift -// Adobe Downloader -// -// Created by X1a0He on 11/13/24. -// - -import Cocoa - -extension PrivilegedHelperManager { - func getInstallScript() -> String { - let appPath = Bundle.main.bundlePath - let bash = """ - #!/bin/bash - set -e - - plistPath=/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist - rm -rf /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) - if [ -e ${plistPath} ]; then - launchctl unload -w ${plistPath} - rm ${plistPath} - fi - launchctl remove \(PrivilegedHelperManager.machServiceName) || true - - mkdir -p /Library/PrivilegedHelperTools/ - rm -f /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) - - cp "\(appPath)/Contents/Library/LaunchServices/\(PrivilegedHelperManager.machServiceName)" "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)" - - echo ' - - - - - Label - \(PrivilegedHelperManager.machServiceName) - MachServices - - \(PrivilegedHelperManager.machServiceName) - - - Program - /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) - ProgramArguments - - /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) - - - - ' > ${plistPath} - - launchctl load -w ${plistPath} - """ - return bash - } - - func runScriptWithRootPermission(script: String) { - let tmpPath = FileManager.default.temporaryDirectory.appendingPathComponent(NSUUID().uuidString).appendingPathExtension("sh") - do { - try script.write(to: tmpPath, atomically: true, encoding: .utf8) - let appleScriptStr = "do shell script \"bash \(tmpPath.path) \" with administrator privileges" - let appleScript = NSAppleScript(source: appleScriptStr) - var dict: NSDictionary? - if appleScript?.executeAndReturnError(&dict) == nil { - - } else { - - } - } catch let err { - print("legacyInstallHelper create script fail: \(err)") - } - try? FileManager.default.removeItem(at: tmpPath) - } - - func legacyInstallHelper() { - defer { - Thread.sleep(forTimeInterval: 1) - } - let script = getInstallScript() - runScriptWithRootPermission(script: script) - } - - func removeInstallHelper() { - defer { - Thread.sleep(forTimeInterval: 5) - } - let script = """ - /bin/launchctl remove \(PrivilegedHelperManager.machServiceName) || true - /usr/bin/killall -u root -9 \(PrivilegedHelperManager.machServiceName) - /bin/rm -rf /Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist - /bin/rm -rf /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) - """ - runScriptWithRootPermission(script: script) - } -} diff --git a/Adobe Downloader/Info.plist b/Adobe Downloader/Info.plist index 1dc3f65..9e02583 100644 --- a/Adobe Downloader/Info.plist +++ b/Adobe Downloader/Info.plist @@ -12,7 +12,7 @@ SMPrivilegedExecutables com.x1a0he.macOS.Adobe-Downloader.helper - identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic + identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he0907@gmail.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ SUFeedURL https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml @@ -28,7 +28,7 @@ com.apple.security.temporary-exception.files.home-relative-path.read-write - /Downloads/ + / diff --git a/Adobe Downloader/NetworkManager.swift b/Adobe Downloader/NetworkManager.swift index 9b123ca..160dfb7 100644 --- a/Adobe Downloader/NetworkManager.swift +++ b/Adobe Downloader/NetworkManager.swift @@ -32,6 +32,7 @@ class NetworkManager: ObservableObject { private let installManager = InstallManager() @AppStorage("defaultDirectory") private var defaultDirectory: String = "" @AppStorage("useDefaultDirectory") private var useDefaultDirectory: Bool = true + @AppStorage("apiVersion") private var apiVersion: String = "6" enum InstallationState { case idle @@ -55,7 +56,18 @@ class NetworkManager: ObservableObject { } func fetchProducts() async { - await fetchProductsWithRetry() + loadingState = .loading + do { + let products = try await fetchProductsWithVersion(apiVersion) + await MainActor.run { + self.saps = products + self.loadingState = .success + } + } catch { + await MainActor.run { + self.loadingState = .failed(error) + } + } } func startDownload(sap: Sap, selectedVersion: String, language: String, destinationURL: URL) async throws { guard let productInfo = self.saps[sap.sapCode]?.versions[selectedVersion] else { @@ -366,4 +378,45 @@ class NetworkManager: ObservableObject { downloadTasks.append(contentsOf: savedTasks) updateDockBadge() } + + private func fetchProductsWithVersion(_ version: String) async throws -> [String: Sap] { + var components = URLComponents(string: NetworkConstants.productsXmlURL) + components?.queryItems = [ + URLQueryItem(name: "_type", value: "xml"), + URLQueryItem(name: "channel", value: "ccm"), + URLQueryItem(name: "channel", value: "sti"), + URLQueryItem(name: "platform", value: "osx10-64,osx10,macarm64,macuniversal"), + URLQueryItem(name: "productType", value: "Desktop"), + URLQueryItem(name: "version", value: version) + ] + + guard let url = components?.url else { + throw NetworkError.invalidURL(NetworkConstants.productsXmlURL) + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw NetworkError.invalidResponse + } + + guard (200...299).contains(httpResponse.statusCode) else { + throw NetworkError.httpError(httpResponse.statusCode, nil) + } + + guard let xmlString = String(data: data, encoding: .utf8) else { + throw NetworkError.invalidData("无法解码XML数据") + } + + let result = try await Task.detached(priority: .userInitiated) { + let parseResult = try XHXMLParser.parse(xmlString: xmlString) + return parseResult.products + }.value + + return result + } } diff --git a/Adobe Downloader/Views/AboutView.swift b/Adobe Downloader/Views/AboutView.swift index 1393ba4..1719433 100644 --- a/Adobe Downloader/Views/AboutView.swift +++ b/Adobe Downloader/Views/AboutView.swift @@ -6,6 +6,27 @@ import SwiftUI import Sparkle +import Combine + +struct PulsingCircle: View { + let color: Color + @State private var scale: CGFloat = 1.0 + + var body: some View { + Circle() + .fill(color) + .frame(width: 8, height: 8) + .scaleEffect(scale) + .animation( + Animation.easeInOut(duration: 1.0) + .repeatForever(autoreverses: true), + value: scale + ) + .onAppear { + scale = 1.5 + } + } +} struct AboutView: View { private let updater: SPUUpdater @@ -56,6 +77,16 @@ final class GeneralSettingsViewModel: ObservableObject { @Published var isCancelled = false + @Published private(set) var helperConnectionStatus: HelperConnectionStatus = .connecting + private var cancellables = Set() + + enum HelperConnectionStatus { + case connected + case connecting + case disconnected + case checking + } + let updater: SPUUpdater init(updater: SPUUpdater) { @@ -63,6 +94,30 @@ final class GeneralSettingsViewModel: ObservableObject { self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates self.setupVersion = ModifySetup.checkComponentVersion() + + self.helperConnectionStatus = .connecting + + PrivilegedHelperManager.shared.$connectionState + .receive(on: DispatchQueue.main) + .sink { [weak self] state in + switch state { + case .connected: + self?.helperConnectionStatus = .connected + case .disconnected: + self?.helperConnectionStatus = .disconnected + case .connecting: + self?.helperConnectionStatus = .connecting + } + } + .store(in: &cancellables) + + DispatchQueue.main.async { + PrivilegedHelperManager.shared.executeCommand("whoami") { _ in } + } + } + + deinit { + cancellables.removeAll() } func updateAutomaticallyChecksForUpdates(_ newValue: Bool) { @@ -89,11 +144,40 @@ struct GeneralSettingsView: View { @AppStorage("downloadAppleSilicon") private var downloadAppleSilicon: Bool = true @EnvironmentObject private var networkManager: NetworkManager @StateObject private var viewModel: GeneralSettingsViewModel + @State private var isReinstallingHelper = false + @State private var showHelperAlert = false + @State private var helperAlertMessage = "" + @State private var helperAlertSuccess = false + @AppStorage("apiVersion") private var apiVersion: String = "6" init(updater: SPUUpdater) { _viewModel = StateObject(wrappedValue: GeneralSettingsViewModel(updater: updater)) } + private var helperStatusColor: Color { + switch viewModel.helperConnectionStatus { + case .connected: + return .green + case .connecting, .checking: + return .orange + case .disconnected: + return .red + } + } + + private var helperStatusText: String { + switch viewModel.helperConnectionStatus { + case .connected: + return "运行正常" + case .connecting: + return "正在连接" + case .checking: + return "检查中" + case .disconnected: + return "连接断开" + } + } + var body: some View { Form { GroupBox(label: Text("下载设置").padding(.bottom, 8)) { @@ -151,11 +235,75 @@ struct GeneralSettingsView: View { } GroupBox(label: Text("其他设置").padding(.bottom, 8)) { VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Helper 安装状态: ") + if PrivilegedHelperManager.getHelperStatus { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("已安装") + } else { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + Text("未安装") + .foregroundColor(.red) + } + Spacer() + + if isReinstallingHelper { + ProgressView() + .scaleEffect(0.7) + .frame(width: 16, height: 16) + } + + Button(action: { + isReinstallingHelper = true + PrivilegedHelperManager.shared.reinstallHelper { success, message in + helperAlertSuccess = success + helperAlertMessage = message + showHelperAlert = true + isReinstallingHelper = false + } + }) { + Text("重新安装") + } + .disabled(isReinstallingHelper) + } + + if !PrivilegedHelperManager.getHelperStatus { + Text("Helper未安装将导致无法执行需要管理员权限的操作") + .font(.caption) + .foregroundColor(.red) + } + } + Divider() + HStack { + Text("Helper 当前状态: ") + PulsingCircle(color: helperStatusColor) + .padding(.horizontal, 4) + Text(helperStatusText) + .foregroundColor(helperStatusColor) + + Spacer() + + Button(action: { + PrivilegedHelperManager.shared.reconnectHelper { success, message in + helperAlertSuccess = success + helperAlertMessage = message + showHelperAlert = true + } + }) { + Text("重新连接Helper") + } + .disabled(isReinstallingHelper) + } + Divider() HStack { Text("Setup 组件状态: ") if ModifySetup.isSetupBackup() { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) + Text("已备份处理") } else { Image(systemName: "xmark.circle.fill") .foregroundColor(.red) @@ -308,6 +456,11 @@ struct GeneralSettingsView: View { } message: { Text("确定要重新处理 Setup 组件吗?这个操作需要管理员权限。") } + .alert(helperAlertSuccess ? "操作成功" : "操作失败", isPresented: $showHelperAlert) { + Button("确定") { } + } message: { + Text(helperAlertMessage) + } .task { viewModel.setupVersion = ModifySetup.checkComponentVersion() networkManager.updateAllowedPlatform(useAppleSilicon: downloadAppleSilicon) diff --git a/Adobe Downloader/Views/SettingsView.swift b/Adobe Downloader/Views/SettingsView.swift deleted file mode 100644 index 88c2b79..0000000 --- a/Adobe Downloader/Views/SettingsView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Adobe Downloader -// -// Created by X1a0He on 2024/10/30. -// -import SwiftUI - -struct SettingsView: View { - @AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN" - @AppStorage("defaultDirectory") private var defaultDirectory: String = "" - @Binding var useDefaultLanguage: Bool - @Binding var useDefaultDirectory: Bool - - var onSelectLanguage: () -> Void - var onSelectDirectory: () -> Void - - private let languageMap: [(code: String, name: String)] = AppStatics.supportedLanguages - - var body: some View { - VStack() { - HStack() { - HStack() { - Toggle(isOn: $useDefaultLanguage) { - Text("语言:") - .fixedSize() - } - .toggleStyle(.checkbox) - .fixedSize() - - Text(getLanguageName(code: defaultLanguage)) - .foregroundColor(.secondary) - .lineLimit(1) - .frame(alignment: .leading) - Spacer() - Button("选择", action: onSelectLanguage) - .fixedSize() - } - - Divider() - .frame(height: 16) - - HStack() { - Toggle(isOn: $useDefaultDirectory) { - Text("目录:") - .fixedSize() - } - .toggleStyle(.checkbox) - .fixedSize() - - Text(formatPath(defaultDirectory.isEmpty ? String(localized: "未设置") : defaultDirectory)) - .foregroundColor(.secondary) - .lineLimit(1) - .frame(alignment: .leading) - Spacer() - Button("选择", action: onSelectDirectory) - .fixedSize() - } - } - } - .padding() - .fixedSize() - } - - private func getLanguageName(code: String) -> String { - let languageDict = Dictionary(uniqueKeysWithValues: languageMap) - return languageDict[code] ?? code - } - - private func formatPath(_ path: String) -> String { - if path.isEmpty { return String(localized: "未设置") } - let url = URL(fileURLWithPath: path) - return url.lastPathComponent - } -} - -#Preview { - SettingsView( - useDefaultLanguage: .constant(true), - useDefaultDirectory: .constant(true), - onSelectLanguage: {}, - onSelectDirectory: {} - ) -} diff --git a/AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements b/AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements index 0c67376..9ccd9b9 100644 --- a/AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements +++ b/AdobeDownloaderHelperTool/AdobeDownloaderHelperTool.entitlements @@ -1,5 +1,18 @@ - + + com.apple.security.temporary-exception.files.absolute-path.read-write + + / + + com.apple.security.temporary-exception.files.home-relative-path.read-write + + / + + com.apple.security.application-groups + + com.x1a0he.macOS.Adobe-Downloader + + diff --git a/AdobeDownloaderHelperTool/Info.plist b/AdobeDownloaderHelperTool/Info.plist index ae0a97d..afa17e3 100644 --- a/AdobeDownloaderHelperTool/Info.plist +++ b/AdobeDownloaderHelperTool/Info.plist @@ -14,7 +14,12 @@ 100 SMAuthorizedClients - identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic + identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: x1a0he0907@gmail.com (LFN2762T4F)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ + MachServices + + com.x1a0he.macOS.Adobe-Downloader.helper + + \ No newline at end of file diff --git a/AdobeDownloaderHelperTool/Launchd.plist b/AdobeDownloaderHelperTool/Launchd.plist index 2bfbd97..263726d 100644 --- a/AdobeDownloaderHelperTool/Launchd.plist +++ b/AdobeDownloaderHelperTool/Launchd.plist @@ -9,7 +9,19 @@ com.x1a0he.macOS.Adobe-Downloader.helper - AssociatedBundleIdentifiers - com.x1a0he.macOS.Adobe-Downloader + Program + /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper + ProgramArguments + + /Library/PrivilegedHelperTools/com.x1a0he.macOS.Adobe-Downloader.helper + + RunAtLoad + + KeepAlive + + StandardErrorPath + /tmp/com.x1a0he.macOS.Adobe-Downloader.helper.err + StandardOutPath + /tmp/com.x1a0he.macOS.Adobe-Downloader.helper.out \ No newline at end of file diff --git a/AdobeDownloaderHelperTool/main.swift b/AdobeDownloaderHelperTool/main.swift index 9077c34..8b35487 100644 --- a/AdobeDownloaderHelperTool/main.swift +++ b/AdobeDownloaderHelperTool/main.swift @@ -7,5 +7,88 @@ import Foundation -print("Hello, World!") +@objc(HelperToolProtocol) protocol HelperToolProtocol { + func executeCommand(_ command: String, withReply reply: @escaping (String) -> Void) +} + +class HelperTool: NSObject, HelperToolProtocol { + private let listener: NSXPCListener + private var connections: Set = [] + + override init() { + listener = NSXPCListener(machServiceName: "com.x1a0he.macOS.Adobe-Downloader.helper") + super.init() + listener.delegate = self + } + + func run() { + ProcessInfo.processInfo.disableSuddenTermination() + ProcessInfo.processInfo.disableAutomaticTermination("Helper is running") + + listener.resume() + + RunLoop.current.run() + } + + func executeCommand(_ command: String, withReply reply: @escaping (String) -> Void) { + print("[Adobe Downloader Helper] 收到执行命令请求: \(command)") + print("[Adobe Downloader Helper] 当前进程权限: \(geteuid())") + + let task = Process() + let pipe = Pipe() + + task.standardOutput = pipe + task.standardError = pipe + task.arguments = ["-c", command] + task.executableURL = URL(fileURLWithPath: "/bin/sh") + + do { + print("[Adobe Downloader Helper] 开始执行命令") + try task.run() + task.waitUntilExit() + + let status = task.terminationStatus + print("[Adobe Downloader Helper] 命令执行完成,退出状态: \(status)") + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + if let output = String(data: data, encoding: .utf8) { + let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines) + print("[Adobe Downloader Helper] 命令执行成功,输出: \(trimmedOutput)") + reply(trimmedOutput) + } else { + print("[Adobe Downloader Helper] 无法解码命令输出") + reply("Error: Could not decode command output") + } + } catch { + print("[Adobe Downloader Helper] 命令执行失败: \(error)") + reply("Error: \(error.localizedDescription)") + } + } +} + +extension HelperTool: NSXPCListenerDelegate { + func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + newConnection.exportedInterface = NSXPCInterface(with: HelperToolProtocol.self) + newConnection.exportedObject = self + + newConnection.invalidationHandler = { [weak self] in + self?.connections.remove(newConnection) + } + + connections.insert(newConnection) + + newConnection.resume() + return true + } +} + +print("[Adobe Downloader Helper] 开始启动...") + +autoreleasepool { + print("[Adobe Downloader Helper] 初始化 HelperTool...") + let helperTool = HelperTool() + + print("[Adobe Downloader Helper] 运行 Helper 服务...") + helperTool.run() +} diff --git a/Localizables/Localizable.xcstrings b/Localizables/Localizable.xcstrings index e5300f3..994c361 100644 --- a/Localizables/Localizable.xcstrings +++ b/Localizables/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, "(将导致无法使用安装功能)" : { "localizations" : { "en" : { @@ -135,6 +138,9 @@ } } } + }, + "API:" : { + }, "By X1a0He. ❤️ Love from China. ❤️" : { @@ -151,6 +157,15 @@ } } } + }, + "Helper 安装状态: " : { + + }, + "Helper 当前状态: " : { + + }, + "Helper未安装将导致无法执行需要管理员权限的操作" : { + }, "OK" : { @@ -194,6 +209,15 @@ } } } + }, + "v4" : { + + }, + "v5" : { + + }, + "v6" : { + }, "下载" : { "localizations" : { @@ -647,6 +671,9 @@ } } } + }, + "已备份处理" : { + }, "已复制" : { "localizations" : { @@ -667,6 +694,9 @@ } } } + }, + "已安装" : { + }, "已完成" : { "localizations" : { @@ -879,6 +909,9 @@ } } } + }, + "未安装" : { + }, "未对 Setup 组件进行备份处理或者 Setup 组件不存在,无法使用安装功能\n你可以通过设置页面再次对 Setup 组件进行备份处理" : { "localizations" : { @@ -1110,6 +1143,7 @@ } }, "目录:" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1343,6 +1377,7 @@ } }, "语言:" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1453,6 +1488,12 @@ } } } + }, + "重新安装" : { + + }, + "重新连接Helper" : { + }, "重试" : { "localizations" : { diff --git a/update-log.md b/update-log.md index bda816f..45ab559 100644 --- a/update-log.md +++ b/update-log.md @@ -1,5 +1,12 @@ # Change Log +## 2024-11-13 00:00 更新日志 + +```markdown +1. 新增可选API版本 (v4, v5, v6) +2. 引入 Privilege Helper 来处理所有需要权限的操作 +``` + ## 2024-11-11 21:00 更新日志 [//]: # (1.2.0)