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

221 lines
8.3 KiB
Swift
Raw Normal View History

//
// Adobe Downloader
//
// Created by X1a0He on 2024/10/30.
//
import Foundation
actor InstallManager {
enum InstallError: Error, LocalizedError {
case setupNotFound
case installationFailed(String)
case cancelled
case permissionDenied
var errorDescription: String? {
switch self {
case .setupNotFound: return "找不到安装程序"
case .installationFailed(let message): return message
case .cancelled: return "安装已取消"
case .permissionDenied: return "权限被拒绝"
}
}
}
private var installationProcess: Process?
private var progressHandler: ((Double, String) -> Void)?
private let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
private func executeInstallation(
at appPath: URL,
withSudo: Bool = true,
password: String? = nil,
progressHandler: @escaping (Double, String) -> Void,
logHandler: @escaping (String) -> Void
) async throws {
guard FileManager.default.fileExists(atPath: setupPath) else {
throw InstallError.setupNotFound
}
let driverPath = appPath.appendingPathComponent("Contents/Resources/products/driver.xml").path
let installProcess = Process()
if withSudo {
installProcess.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
installProcess.arguments = password != nil ? ["-S"] : []
installProcess.arguments?.append(contentsOf: [setupPath, "--install=1", "--driverXML=\(driverPath)"])
} else {
installProcess.executableURL = URL(fileURLWithPath: setupPath)
installProcess.arguments = ["--install=1", "--driverXML=\(driverPath)"]
}
let outputPipe = Pipe()
installProcess.standardOutput = outputPipe
installProcess.standardError = outputPipe
if let password = password {
let inputPipe = Pipe()
installProcess.standardInput = inputPipe
try inputPipe.fileHandleForWriting.write(contentsOf: "\(password)\n".data(using: .utf8)!)
inputPipe.fileHandleForWriting.closeFile()
}
installationProcess = installProcess
await MainActor.run {
progressHandler(0.0, withSudo ? "正在准备安装..." : "正在重试安装...")
}
try installProcess.run()
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
Task.detached {
do {
for try await line in outputPipe.fileHandleForReading.bytes.lines {
await MainActor.run { logHandler(line) }
if line.contains("incorrect password") || line.contains("sudo: 1 incorrect password attempt") {
installProcess.terminate()
continuation.resume(throwing: InstallError.permissionDenied)
return
}
if let range = line.range(of: "Exit Code: (-?[0-9]+)", options: .regularExpression),
let codeStr = line[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
let code = Int32(codeStr) {
if code != 0 {
let errorMessage = code == -1
? "安装程序调用失败请联系X1a0He"
: "(退出代码: \(code))"
installProcess.terminate()
continuation.resume(throwing: InstallError.installationFailed(errorMessage))
return
}
}
if let progress = await self.parseProgress(from: line) {
await MainActor.run {
progressHandler(progress.progress, progress.status)
}
}
}
installProcess.waitUntilExit()
if installProcess.terminationStatus == 0 {
continuation.resume()
} else {
let errorMessage = withSudo
? "安装失败 (退出代码: \(installProcess.terminationStatus))"
: "重试失败,需要重新输入密码"
continuation.resume(throwing: InstallError.installationFailed(errorMessage))
}
} catch {
continuation.resume(throwing: error)
}
}
}
await MainActor.run {
progressHandler(1.0, "安装完成")
}
}
func install(
at appPath: URL,
progressHandler: @escaping (Double, String) -> Void,
logHandler: @escaping (String) -> Void
) async throws {
self.progressHandler = progressHandler
let password = try await requestPassword()
try await executeInstallation(
at: appPath,
withSudo: true,
password: password,
progressHandler: progressHandler,
logHandler: logHandler
)
}
func retry(
at appPath: URL,
progressHandler: @escaping (Double, String) -> Void,
logHandler: @escaping (String) -> Void
) async throws {
self.progressHandler = progressHandler
try await executeInstallation(
at: appPath,
withSudo: false,
progressHandler: progressHandler,
logHandler: logHandler
)
}
private func requestPassword() async throws -> String {
let authProcess = Process()
authProcess.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
let authScript = """
tell application "System Events"
display dialog "请输入管理员密码以继续安装" default answer "" with hidden answer ¬
buttons {"取消", "确定"} default button "确定" ¬
with icon caution ¬
with title "需要管理员权限"
if button returned of result is "确定" then
return text returned of result
else
error "用户取消了操作"
end if
end tell
"""
let authPipe = Pipe()
authProcess.standardOutput = authPipe
authProcess.standardError = Pipe()
authProcess.arguments = ["-e", authScript]
try authProcess.run()
authProcess.waitUntilExit()
if authProcess.terminationStatus != 0 {
throw InstallError.cancelled
}
guard let passwordData = try? authPipe.fileHandleForReading.readToEnd(),
let password = String(data: passwordData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
!password.isEmpty else {
throw InstallError.cancelled
}
return password
}
func cancel() {
installationProcess?.terminate()
}
private func parseProgress(from line: String) -> (progress: Double, status: String)? {
if let range = line.range(of: "Exit Code: (-?[0-9]+)", options: .regularExpression),
let codeStr = line[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
let exitCode = Int(codeStr) {
return exitCode == 0 ? (1.0, "安装完成") : nil
}
if let range = line.range(of: "Progress: ([0-9]{1,3})%", options: .regularExpression),
let progressStr = line[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
let progressValue = Double(progressStr.replacingOccurrences(of: "%", with: "")) {
return (progressValue / 100.0, "正在安装...")
}
if line.contains("Installing packages") {
return (0.0, "正在安装包...")
} else if line.contains("Preparing") {
return (0.0, "正在准备...")
}
return nil
}
}