2024-10-31 22:35:22 +08:00
|
|
|
//
|
2024-11-05 20:30:18 +08:00
|
|
|
// Adobe Downloader
|
2024-10-31 22:35:22 +08:00
|
|
|
//
|
|
|
|
|
// Created by X1a0He on 2024/10/30.
|
|
|
|
|
//
|
2024-11-06 15:18:57 +08:00
|
|
|
/*
|
|
|
|
|
Adobe Exit Code
|
2024-11-15 17:47:15 +08:00
|
|
|
107: 架构不一致或安装文件被损坏
|
2024-11-06 15:18:57 +08:00
|
|
|
103: 权限问题
|
2025-03-06 01:40:01 +08:00
|
|
|
182: 表示创建的包不包含要安装的包
|
2024-11-15 17:47:15 +08:00
|
|
|
133: 磁盘空间不足
|
2024-11-06 15:18:57 +08:00
|
|
|
*/
|
2024-10-31 22:35:22 +08:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
actor InstallManager {
|
2024-11-04 14:44:52 +08:00
|
|
|
enum InstallError: Error, LocalizedError {
|
2024-10-31 22:35:22 +08:00
|
|
|
case setupNotFound
|
|
|
|
|
case installationFailed(String)
|
|
|
|
|
case cancelled
|
|
|
|
|
case permissionDenied
|
2025-03-27 01:05:20 +08:00
|
|
|
case installationFailedWithDetails(String, String)
|
2024-11-01 17:28:23 +08:00
|
|
|
|
|
|
|
|
var errorDescription: String? {
|
|
|
|
|
switch self {
|
2024-11-07 16:14:42 +08:00
|
|
|
case .setupNotFound: return String(localized: "找不到安装程序")
|
|
|
|
|
case .installationFailed(let message): return message
|
|
|
|
|
case .cancelled: return String(localized: "安装已取消")
|
|
|
|
|
case .permissionDenied: return String(localized: "权限被拒绝")
|
2025-03-27 01:05:20 +08:00
|
|
|
case .installationFailedWithDetails(let message, _): return message
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var installationProcess: Process?
|
|
|
|
|
private var progressHandler: ((Double, String) -> Void)?
|
2024-11-05 20:30:18 +08:00
|
|
|
private let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
|
2024-10-31 22:35:22 +08:00
|
|
|
|
2025-07-11 16:37:40 +08:00
|
|
|
private func terminateSetupProcesses() async {
|
|
|
|
|
let _ = await withCheckedContinuation { continuation in
|
2025-08-17 14:04:13 +08:00
|
|
|
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { result in
|
2025-07-11 16:37:40 +08:00
|
|
|
continuation.resume(returning: result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
actor InstallationState {
|
|
|
|
|
var isCompleted = false
|
|
|
|
|
var error: Error?
|
|
|
|
|
var hasExitCode0 = false
|
|
|
|
|
var lastOutputTime = Date()
|
|
|
|
|
|
|
|
|
|
func markCompleted() {
|
|
|
|
|
isCompleted = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setError(_ error: Error) {
|
|
|
|
|
if !isCompleted {
|
|
|
|
|
self.error = error
|
|
|
|
|
isCompleted = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setExitCode0() {
|
|
|
|
|
hasExitCode0 = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateLastOutputTime() {
|
|
|
|
|
lastOutputTime = Date()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getTimeSinceLastOutput() -> TimeInterval {
|
|
|
|
|
return Date().timeIntervalSince(lastOutputTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var shouldContinue: Bool {
|
|
|
|
|
!isCompleted
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hasReceivedExitCode0: Bool {
|
|
|
|
|
hasExitCode0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-27 01:05:20 +08:00
|
|
|
private func getAdobeInstallLogDetails() async -> String? {
|
|
|
|
|
let logPath = "/Library/Logs/Adobe/Installers/Install.log"
|
|
|
|
|
guard FileManager.default.fileExists(atPath: logPath) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
let logContent = try String(contentsOfFile: logPath, encoding: .utf8)
|
|
|
|
|
let lines = logContent.components(separatedBy: .newlines)
|
|
|
|
|
|
|
|
|
|
let fatalLines = lines.filter {
|
|
|
|
|
line in line.contains("FATAL:")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var uniqueLines: [String] = []
|
|
|
|
|
var seen = Set<String>()
|
|
|
|
|
|
|
|
|
|
for line in fatalLines {
|
|
|
|
|
if !seen.contains(line) {
|
|
|
|
|
seen.insert(line)
|
|
|
|
|
uniqueLines.append(line)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if uniqueLines.isEmpty, lines.count > 10 {
|
|
|
|
|
uniqueLines = Array(lines.suffix(10))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !uniqueLines.isEmpty {
|
|
|
|
|
return uniqueLines.joined(separator: "\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
} catch {
|
|
|
|
|
print("读取安装日志失败: \(error.localizedDescription)")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
private func executeInstallation(
|
|
|
|
|
at appPath: URL,
|
2024-11-14 13:30:13 +08:00
|
|
|
progressHandler: @escaping (Double, String) -> Void
|
2024-11-05 20:30:18 +08:00
|
|
|
) async throws {
|
2024-10-31 22:35:22 +08:00
|
|
|
guard FileManager.default.fileExists(atPath: setupPath) else {
|
|
|
|
|
throw InstallError.setupNotFound
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-06 11:08:44 +08:00
|
|
|
let driverPath = appPath.appendingPathComponent("driver.xml").path
|
2024-11-15 19:34:29 +08:00
|
|
|
guard FileManager.default.fileExists(atPath: driverPath) else {
|
|
|
|
|
throw InstallError.installationFailed("找不到 driver.xml 文件")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let attributes = try? FileManager.default.attributesOfItem(atPath: driverPath)
|
|
|
|
|
if let permissions = attributes?[.posixPermissions] as? NSNumber {
|
|
|
|
|
if permissions.int16Value & 0o444 == 0 {
|
|
|
|
|
throw InstallError.installationFailed("driver.xml 文件没有读取权限")
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-27 01:05:20 +08:00
|
|
|
|
|
|
|
|
await MainActor.run {
|
|
|
|
|
progressHandler(0.0, String(localized: "正在清理安装环境..."))
|
|
|
|
|
}
|
2025-07-11 16:37:40 +08:00
|
|
|
|
|
|
|
|
await terminateSetupProcesses()
|
2025-03-27 01:05:20 +08:00
|
|
|
|
|
|
|
|
let logFiles = [
|
|
|
|
|
"/Library/Logs/Adobe/Installers/Install.log",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for logFile in logFiles {
|
|
|
|
|
let removeCommand = "rm -f '\(logFile)'"
|
|
|
|
|
let result = await withCheckedContinuation { continuation in
|
2025-08-17 14:04:13 +08:00
|
|
|
PrivilegedHelperAdapter.shared.executeCommand(removeCommand) { result in
|
2025-03-27 01:05:20 +08:00
|
|
|
continuation.resume(returning: result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if result.contains("Error") {
|
|
|
|
|
print("清理安装日志失败: \(logFile) - \(result)")
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-15 19:34:29 +08:00
|
|
|
|
2024-11-15 17:47:15 +08:00
|
|
|
let installCommand = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
2024-10-31 22:35:22 +08:00
|
|
|
|
2024-11-01 17:28:23 +08:00
|
|
|
await MainActor.run {
|
2024-11-13 23:56:07 +08:00
|
|
|
progressHandler(0.0, String(localized: "正在准备安装..."))
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
|
|
|
Task.detached {
|
|
|
|
|
do {
|
2025-08-17 14:04:13 +08:00
|
|
|
try await PrivilegedHelperAdapter.shared.executeInstallation(installCommand) { output in
|
2024-11-13 23:56:07 +08:00
|
|
|
Task { @MainActor in
|
2024-11-16 00:51:38 +08:00
|
|
|
if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression),
|
2024-11-13 23:56:07 +08:00
|
|
|
let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
|
|
|
|
|
let exitCode = Int(codeStr) {
|
|
|
|
|
|
|
|
|
|
if exitCode == 0 {
|
|
|
|
|
progressHandler(1.0, String(localized: "安装完成"))
|
2025-08-17 14:04:13 +08:00
|
|
|
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { _ in }
|
2024-11-13 23:56:07 +08:00
|
|
|
continuation.resume()
|
2024-11-16 00:51:38 +08:00
|
|
|
return
|
2024-11-13 23:56:07 +08:00
|
|
|
} else {
|
|
|
|
|
let errorMessage: String
|
2025-03-27 01:05:20 +08:00
|
|
|
errorMessage = String(localized: "错误代码(\(exitCode)),请查看日志详情并向开发者汇报")
|
|
|
|
|
if let logDetails = await self.getAdobeInstallLogDetails() {
|
|
|
|
|
continuation.resume(throwing: InstallError.installationFailedWithDetails(errorMessage, logDetails))
|
|
|
|
|
} else {
|
|
|
|
|
continuation.resume(throwing: InstallError.installationFailed(errorMessage))
|
2024-11-13 23:56:07 +08:00
|
|
|
}
|
2024-11-16 00:51:38 +08:00
|
|
|
return
|
2024-11-13 23:56:07 +08:00
|
|
|
}
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
2024-11-13 23:56:07 +08:00
|
|
|
|
|
|
|
|
if let progress = await self.parseProgress(from: output) {
|
|
|
|
|
progressHandler(progress, String(localized: "正在安装..."))
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
continuation.resume(throwing: error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-13 23:56:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func parseProgress(from output: String) -> Double? {
|
2024-11-16 00:51:38 +08:00
|
|
|
if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression),
|
|
|
|
|
let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
|
|
|
|
|
let exitCode = Int(codeStr) {
|
|
|
|
|
if exitCode == 0 {
|
|
|
|
|
return 1.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if output.range(of: "Progress:\\s*[0-9]+/[0-9]+", options: .regularExpression) != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let range = output.range(of: "Progress:\\s*([0-9]{1,3})%", options: .regularExpression),
|
2024-11-13 23:56:07 +08:00
|
|
|
let progressStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
|
|
|
|
|
let progressValue = Double(progressStr.replacingOccurrences(of: "%", with: "")) {
|
|
|
|
|
return progressValue / 100.0
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
2024-11-13 23:56:07 +08:00
|
|
|
return nil
|
2024-11-01 17:28:23 +08:00
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
|
|
|
|
|
func install(
|
|
|
|
|
at appPath: URL,
|
2024-11-14 13:30:13 +08:00
|
|
|
progressHandler: @escaping (Double, String) -> Void
|
2024-11-05 20:30:18 +08:00
|
|
|
) async throws {
|
|
|
|
|
try await executeInstallation(
|
|
|
|
|
at: appPath,
|
2024-11-14 13:30:13 +08:00
|
|
|
progressHandler: progressHandler
|
2024-11-05 20:30:18 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-13 23:56:07 +08:00
|
|
|
func cancel() {
|
2025-08-17 14:04:13 +08:00
|
|
|
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { _ in }
|
2024-11-13 23:56:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getInstallCommand(for driverPath: String) -> String {
|
|
|
|
|
return "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
func retry(
|
|
|
|
|
at appPath: URL,
|
2024-11-14 13:30:13 +08:00
|
|
|
progressHandler: @escaping (Double, String) -> Void
|
2024-11-05 20:30:18 +08:00
|
|
|
) async throws {
|
2024-11-15 17:47:15 +08:00
|
|
|
cancel()
|
|
|
|
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
try await executeInstallation(
|
|
|
|
|
at: appPath,
|
2024-11-14 13:30:13 +08:00
|
|
|
progressHandler: progressHandler
|
2024-11-05 20:30:18 +08:00
|
|
|
)
|
|
|
|
|
}
|
2024-11-13 23:56:07 +08:00
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
|