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
|
|
|
|
|
|
107: 架构或者版本不一致
|
|
|
|
|
|
103: 权限问题
|
|
|
|
|
|
182: 可能是文件不全或者出错了
|
|
|
|
|
|
*/
|
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
|
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: "权限被拒绝")
|
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
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
|
private func executeInstallation(
|
|
|
|
|
|
at appPath: URL,
|
|
|
|
|
|
withSudo: Bool = true,
|
|
|
|
|
|
password: String? = nil,
|
|
|
|
|
|
progressHandler: @escaping (Double, String) -> Void,
|
|
|
|
|
|
logHandler: @escaping (String) -> Void
|
|
|
|
|
|
) 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-05 20:30:18 +08:00
|
|
|
|
let installProcess = Process()
|
2024-10-31 22:35:22 +08:00
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
|
if withSudo {
|
2024-11-01 17:28:23 +08:00
|
|
|
|
installProcess.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
|
2024-11-05 20:30:18 +08:00
|
|
|
|
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)"]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-06 15:18:57 +08:00
|
|
|
|
let commandString = "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
|
|
|
|
|
print(commandString)
|
|
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
|
let outputPipe = Pipe()
|
|
|
|
|
|
installProcess.standardOutput = outputPipe
|
|
|
|
|
|
installProcess.standardError = outputPipe
|
2024-11-04 17:40:01 +08:00
|
|
|
|
|
2024-11-05 20:30:18 +08:00
|
|
|
|
if let password = password {
|
2024-11-01 17:28:23 +08:00
|
|
|
|
let inputPipe = Pipe()
|
|
|
|
|
|
installProcess.standardInput = inputPipe
|
2024-10-31 22:35:22 +08:00
|
|
|
|
try inputPipe.fileHandleForWriting.write(contentsOf: "\(password)\n".data(using: .utf8)!)
|
|
|
|
|
|
inputPipe.fileHandleForWriting.closeFile()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-01 17:28:23 +08:00
|
|
|
|
installationProcess = installProcess
|
|
|
|
|
|
|
|
|
|
|
|
await MainActor.run {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
progressHandler(0.0, withSudo ? String(localized: "正在准备安装...") : String(localized: "正在重试安装..."))
|
2024-11-01 17:28:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try installProcess.run()
|
|
|
|
|
|
|
|
|
|
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
|
|
|
|
Task.detached {
|
|
|
|
|
|
do {
|
|
|
|
|
|
for try await line in outputPipe.fileHandleForReading.bytes.lines {
|
2024-11-05 20:30:18 +08:00
|
|
|
|
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
|
2024-11-04 17:40:01 +08:00
|
|
|
|
}
|
2024-11-01 17:28:23 +08:00
|
|
|
|
|
|
|
|
|
|
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 {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
let errorMessage = code == -1
|
|
|
|
|
|
? String(localized: "安装程序调用失败,请联系X1a0He")
|
|
|
|
|
|
: String(localized: "(退出代码: \(code))")
|
|
|
|
|
|
|
2024-11-01 17:28:23 +08:00
|
|
|
|
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 {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
let errorMessage = withSudo
|
|
|
|
|
|
? String(localized: "(退出代码: \(installProcess.terminationStatus))")
|
|
|
|
|
|
: String(localized: "重试失败,需要重新输入密码")
|
2024-11-05 20:30:18 +08:00
|
|
|
|
continuation.resume(throwing: InstallError.installationFailed(errorMessage))
|
2024-11-01 17:28:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
continuation.resume(throwing: error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await MainActor.run {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
progressHandler(1.0, String(localized: "安装完成"))
|
2024-11-01 17:28:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-11-05 20:30:18 +08:00
|
|
|
|
|
|
|
|
|
|
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"
|
2024-11-07 16:14:42 +08:00
|
|
|
|
display dialog "请输入管理员密码以继续安装(Please enter the password to continue the installation)" default answer "" with hidden answer ¬
|
2024-11-05 20:30:18 +08:00
|
|
|
|
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) {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
return exitCode == 0 ? (1.0, String(localized: "安装完成")) : nil
|
2024-11-05 20:30:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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: "")) {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
return (progressValue / 100.0, String("正在安装..."))
|
2024-11-05 20:30:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if line.contains("Installing packages") {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
return (0.0, String(localized: "正在安装包..."))
|
2024-11-05 20:30:18 +08:00
|
|
|
|
} else if line.contains("Preparing") {
|
2024-11-07 16:14:42 +08:00
|
|
|
|
return (0.0, String(localized: "正在准备..."))
|
2024-11-05 20:30:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2024-11-07 16:14:42 +08:00
|
|
|
|
|
|
|
|
|
|
func getInstallCommand(for driverPath: String) -> String {
|
|
|
|
|
|
return "sudo \"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
|
|
|
|
|
|
}
|
2024-10-31 22:35:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|