feat: Add Helper status monitoring.

This commit is contained in:
X1a0He
2024-11-13 13:20:25 +08:00
parent 1546dd6af4
commit 95a534c007
17 changed files with 597 additions and 204 deletions

View File

@@ -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;

View File

@@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"

View File

@@ -51,6 +51,10 @@ struct Adobe_DownloaderApp: App {
}
}
PrivilegedHelperManager.shared.checkInstall()
if UserDefaults.standard.string(forKey: "apiVersion") == nil {
UserDefaults.standard.set("6", forKey: "apiVersion")
}
}
var body: some Scene {
@@ -188,6 +192,7 @@ struct Adobe_DownloaderApp: App {
CheckForUpdatesView(updater: updaterController.updater)
}
}
Settings {
AboutView(updater: updaterController.updater)
.environmentObject(networkManager)

View File

@@ -194,8 +194,11 @@ struct NetworkConstants {
static let maxConcurrentDownloads = 3
static let progressUpdateInterval: TimeInterval = 1
static var productsXmlURL: String {
"https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v\(UserDefaults.standard.string(forKey: "apiVersion") ?? "6")/products/all"
}
static let applicationJsonURL = "https://cdn-ffc.oobesaas.adobe.com/core/v3/applications"
static let productsXmlURL = "https://prod-rel-ffc-ccm.oobesaas.adobe.com/adobe-ffc-external/core/v6/products/all"
static let adobeRequestHeaders = [
"X-Adobe-App-Id": "accc-apps-panel-desktop",

View File

@@ -6,6 +6,7 @@ struct ContentView: View {
@State private var errorMessage: String?
@State private var showDownloadManager = false
@State private var searchText = ""
@AppStorage("apiVersion") private var apiVersion: String = "6"
private var filteredProducts: [Sap] {
let products = networkManager.saps.values
@@ -35,7 +36,23 @@ struct ContentView: View {
.fixedSize()
Spacer()
HStack {
Text("API:")
.foregroundColor(.secondary)
Picker("", selection: $apiVersion) {
Text("v4").tag("4")
Text("v5").tag("5")
Text("v6").tag("6")
}
.pickerStyle(.segmented)
.frame(width: 150)
.onChange(of: apiVersion) { newValue in
refreshData()
}
}
.padding(.horizontal, 10)
HStack(spacing: 8) {
SearchField(text: $searchText)
.frame(maxWidth: 200)
@@ -169,7 +186,6 @@ struct ContentView: View {
.environmentObject(networkManager)
}
.onAppear {
if networkManager.saps.isEmpty {
refreshData()
}

View File

@@ -9,6 +9,10 @@ import AppKit
import Cocoa
import ServiceManagement
@objc protocol HelperToolProtocol {
func executeCommand(_ command: String, withReply reply: @escaping (String) -> 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<CFError>?
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
}

View File

@@ -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 '
<?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>\(PrivilegedHelperManager.machServiceName)</string>
<key>MachServices</key>
<dict>
<key>\(PrivilegedHelperManager.machServiceName)</key>
<true/>
</dict>
<key>Program</key>
<string>/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)</string>
<key>ProgramArguments</key>
<array>
<string>/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)</string>
</array>
</dict>
</plist>
' > ${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)
}
}

View File

@@ -12,7 +12,7 @@
<key>SMPrivilegedExecutables</key>
<dict>
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<string>identifier "com.x1a0he.macOS.Adobe-Downloader.helper" and anchor apple generic</string>
<string>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 */</string>
</dict>
<key>SUFeedURL</key>
<string>https://raw.githubusercontent.com/X1a0He/Adobe-Downloader/refs/heads/main/appcast.xml</string>
@@ -28,7 +28,7 @@
<true/>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/Downloads/</string>
<string>/</string>
</array>
</dict>
</plist>

View File

@@ -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
}
}

View File

@@ -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<AnyCancellable>()
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)

View File

@@ -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: {}
)
}

View File

@@ -1,5 +1,18 @@
<?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/>
<dict>
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
<string>/</string>
</array>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>com.x1a0he.macOS.Adobe-Downloader</string>
</array>
</dict>
</plist>

View File

@@ -14,7 +14,12 @@
<string>100</string>
<key>SMAuthorizedClients</key>
<array>
<string>identifier "com.x1a0he.macOS.Adobe-Downloader" and anchor apple generic</string>
<string>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 */</string>
</array>
<key>MachServices</key>
<dict>
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -9,7 +9,19 @@
<key>com.x1a0he.macOS.Adobe-Downloader.helper</key>
<true/>
</dict>
<key>AssociatedBundleIdentifiers</key>
<string>com.x1a0he.macOS.Adobe-Downloader</string>
<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>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>

View File

@@ -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<NSXPCConnection> = []
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()
}

View File

@@ -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" : {

View File

@@ -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)