mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 11:18:53 +08:00
234 lines
9.0 KiB
Swift
234 lines
9.0 KiB
Swift
//
|
||
// Adobe Downloader
|
||
//
|
||
// Created by X1a0He on 2024/10/30.
|
||
//
|
||
/*
|
||
Adobe Exit Code
|
||
107: 架构或者版本不一致
|
||
103: 权限问题
|
||
182: 可能是文件不全或者出错了
|
||
*/
|
||
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 String(localized: "找不到安装程序")
|
||
case .installationFailed(let message): return message
|
||
case .cancelled: return String(localized: "安装已取消")
|
||
case .permissionDenied: return String(localized: "权限被拒绝")
|
||
}
|
||
}
|
||
}
|
||
|
||
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("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 commandString = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||
print(commandString)
|
||
|
||
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 ? String(localized: "正在准备安装...") : String(localized: "正在重试安装..."))
|
||
}
|
||
|
||
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
|
||
? String(localized: "安装程序调用失败,请联系X1a0He")
|
||
: String(localized: "(退出代码: \(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
|
||
? String(localized: "(退出代码: \(installProcess.terminationStatus))")
|
||
: String(localized: "重试失败,需要重新输入密码")
|
||
continuation.resume(throwing: InstallError.installationFailed(errorMessage))
|
||
}
|
||
} catch {
|
||
continuation.resume(throwing: error)
|
||
}
|
||
}
|
||
}
|
||
|
||
await MainActor.run {
|
||
progressHandler(1.0, String(localized: "安装完成"))
|
||
}
|
||
}
|
||
|
||
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 "请输入管理员密码以继续安装(Please enter the password to continue the installation)" 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, String(localized: "安装完成")) : 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, String("正在安装..."))
|
||
}
|
||
|
||
if line.contains("Installing packages") {
|
||
return (0.0, String(localized: "正在安装包..."))
|
||
} else if line.contains("Preparing") {
|
||
return (0.0, String(localized: "正在准备..."))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func getInstallCommand(for driverPath: String) -> String {
|
||
return "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
||
}
|
||
}
|
||
|