Files
Adobe-Downloader/Adobe Downloader/Utils/InstallManager.swift

265 lines
9.3 KiB
Swift

//
// Adobe Downloader
//
// Created by X1a0He on 2024/10/30.
//
/*
Adobe Exit Code
107:
103:
182:
133:
*/
import Foundation
actor InstallManager {
enum InstallError: Error, LocalizedError {
case setupNotFound
case installationFailed(String)
case cancelled
case permissionDenied
case installationFailedWithDetails(String, String)
var errorDescription: String? {
switch self {
case .setupNotFound: return String(localized: "找不到安装程序")
case .installationFailed(let message): return message
case .cancelled: return String(localized: "安装已取消")
case .permissionDenied: return String(localized: "权限被拒绝")
case .installationFailedWithDetails(let message, _): return message
}
}
}
private var installationProcess: Process?
private var progressHandler: ((Double, String) -> Void)?
private let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
private func terminateSetupProcesses() async {
let _ = await withCheckedContinuation { continuation in
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { result in
continuation.resume(returning: result)
}
}
try? await Task.sleep(nanoseconds: 500_000_000)
}
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
}
}
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
}
}
private func executeInstallation(
at appPath: URL,
progressHandler: @escaping (Double, String) -> Void
) async throws {
guard FileManager.default.fileExists(atPath: setupPath) else {
throw InstallError.setupNotFound
}
let driverPath = appPath.appendingPathComponent("driver.xml").path
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 文件没有读取权限")
}
}
await MainActor.run {
progressHandler(0.0, String(localized: "正在清理安装环境..."))
}
await terminateSetupProcesses()
let logFiles = [
"/Library/Logs/Adobe/Installers/Install.log",
]
for logFile in logFiles {
let removeCommand = "rm -f '\(logFile)'"
let result = await withCheckedContinuation { continuation in
PrivilegedHelperAdapter.shared.executeCommand(removeCommand) { result in
continuation.resume(returning: result)
}
}
if result.contains("Error") {
print("清理安装日志失败: \(logFile) - \(result)")
}
}
let installCommand = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
await MainActor.run {
progressHandler(0.0, String(localized: "正在准备安装..."))
}
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
Task.detached {
do {
try await PrivilegedHelperAdapter.shared.executeInstallation(installCommand) { output in
Task { @MainActor in
if let range = output.range(of: "Exit Code:\\s*(-?[0-9]+)", options: .regularExpression),
let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
let exitCode = Int(codeStr) {
if exitCode == 0 {
progressHandler(1.0, String(localized: "安装完成"))
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { _ in }
continuation.resume()
return
} else {
let errorMessage: String
errorMessage = String(localized: "错误代码(\(exitCode)),请查看日志详情并向开发者汇报")
if let logDetails = await self.getAdobeInstallLogDetails() {
continuation.resume(throwing: InstallError.installationFailedWithDetails(errorMessage, logDetails))
} else {
continuation.resume(throwing: InstallError.installationFailed(errorMessage))
}
return
}
}
if let progress = await self.parseProgress(from: output) {
progressHandler(progress, String(localized: "正在安装..."))
}
}
}
} catch {
continuation.resume(throwing: error)
}
}
}
}
private func parseProgress(from output: String) -> Double? {
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),
let progressStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
let progressValue = Double(progressStr.replacingOccurrences(of: "%", with: "")) {
return progressValue / 100.0
}
return nil
}
func install(
at appPath: URL,
progressHandler: @escaping (Double, String) -> Void
) async throws {
try await executeInstallation(
at: appPath,
progressHandler: progressHandler
)
}
func cancel() {
PrivilegedHelperAdapter.shared.executeCommand("pkill -f Setup") { _ in }
}
func getInstallCommand(for driverPath: String) -> String {
return "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
}
func retry(
at appPath: URL,
progressHandler: @escaping (Double, String) -> Void
) async throws {
cancel()
try await Task.sleep(nanoseconds: 1_000_000_000)
try await executeInstallation(
at: appPath,
progressHandler: progressHandler
)
}
}