mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 03:14:57 +08:00
feat: Add Helper status monitoring.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: {}
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user