feat: Call Setup through Helper to install and get progress.

This commit is contained in:
X1a0He
2024-11-13 23:56:07 +08:00
parent 4753212a74
commit c44b3f3c9c
6 changed files with 196 additions and 273 deletions

View File

@@ -30,9 +30,9 @@
filePath = "Adobe Downloader/Utils/InstallManager.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "143"
endingLineNumber = "143"
landmarkName = "install(at:progressHandler:logHandler:)"
startingLineNumber = "132"
endingLineNumber = "132"
landmarkName = "retry(at:progressHandler:logHandler:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>

View File

@@ -11,6 +11,8 @@ import ServiceManagement
@objc protocol HelperToolProtocol {
func executeCommand(_ command: String, withReply reply: @escaping (String) -> Void)
func startInstallation(_ command: String, withReply reply: @escaping (String) -> Void)
func getInstallationOutput(withReply reply: @escaping (String) -> Void)
}
@objcMembers
@@ -198,7 +200,7 @@ class PrivilegedHelperManager: NSObject {
completion(false, "获取授权失败")
case .getAdminFail:
completion(false, "获取管理员权限失败")
case let .blessError(code):
case .blessError(_):
completion(false, "安装失败: \(result.alertContent)")
}
}
@@ -323,6 +325,44 @@ class PrivilegedHelperManager: NSObject {
}
}
}
func executeInstallation(_ command: String, progress: @escaping (String) -> Void) async throws {
guard let connection = connectToHelper() else {
throw NSError(domain: "HelperError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not connect to helper"])
}
guard let helper = connection.remoteObjectProxyWithErrorHandler({ error in
self.connectionState = .disconnected
}) as? HelperToolProtocol else {
throw NSError(domain: "HelperError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Could not get helper proxy"])
}
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
helper.startInstallation(command) { result in
if result == "Started" {
continuation.resume()
} else {
continuation.resume(throwing: NSError(domain: "HelperError", code: -3, userInfo: [NSLocalizedDescriptionKey: result]))
}
}
}
while true {
let output = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, Error>) in
helper.getInstallationOutput { result in
continuation.resume(returning: result)
}
}
if output == "Completed" {
break
} else if !output.isEmpty {
progress(output)
}
try await Task.sleep(nanoseconds: 100_000_000)
}
}
}
extension PrivilegedHelperManager {

View File

@@ -34,8 +34,6 @@ actor InstallManager {
private func executeInstallation(
at appPath: URL,
withSudo: Bool = true,
password: String? = nil,
progressHandler: @escaping (Double, String) -> Void,
logHandler: @escaping (String) -> Void
) async throws {
@@ -44,89 +42,64 @@ actor InstallManager {
}
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
let installCommand = "\"\(setupPath)\" --install=1 --driverXML=\"\(driverPath)\""
await MainActor.run {
progressHandler(0.0, withSudo ? String(localized: "正在准备安装...") : String(localized: "正在重试安装..."))
progressHandler(0.0, 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) }
try await PrivilegedHelperManager.shared.executeInstallation(installCommand) { output in
Task { @MainActor in
logHandler(output)
if let range = output.range(of: "Exit Code: (-?[0-9]+)", options: .regularExpression),
let codeStr = output[range].split(separator: ":").last?.trimmingCharacters(in: .whitespaces),
let exitCode = Int(codeStr) {
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: "安装程序调用失败Setup 组件未被处理,请联系开发者")
: 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 {
if exitCode == 0 {
progressHandler(1.0, String(localized: "安装完成"))
continuation.resume()
} else {
let errorMessage = String(localized: "(退出代码: \(installProcess.terminationStatus))")
let errorMessage: String
switch exitCode {
case 107:
errorMessage = String(localized: "安装失败: 架构或版本不一致 (退出代码: \(exitCode))")
case 103:
errorMessage = String(localized: "安装失败: 权限问题 (退出代码: \(exitCode))")
case 182:
errorMessage = String(localized: "安装失败: 安装文件不完整或损坏 (退出代码: \(exitCode))")
case -1:
errorMessage = String(localized: "安装失败: Setup 组件未被处理 (退出代码: \(exitCode))")
default:
errorMessage = String(localized: "安装失败 (退出代码: \(exitCode))")
}
progressHandler(0.0, errorMessage)
continuation.resume(throwing: InstallError.installationFailed(errorMessage))
}
return
}
if let progress = await self.parseProgress(from: output) {
progressHandler(progress, String(localized: "正在安装..."))
}
}
}
} catch {
continuation.resume(throwing: error)
}
}
}
await MainActor.run {
progressHandler(1.0, String(localized: "安装完成"))
}
private func parseProgress(from output: String) -> Double? {
if let range = output.range(of: "Progress: ([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(
@@ -134,100 +107,31 @@ actor InstallManager {
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 cancel() {
PrivilegedHelperManager.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,
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
)
}
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)\""
}
}

View File

@@ -93,23 +93,14 @@ class ModifySetup {
let setupPath = "/Library/Application Support/Adobe/Adobe Desktop Common/HDBox/Setup"
let commands = [
//
"""
perl -0777pi -e 'BEGIN{$/=\\1e8} s|\\x55\\x48\\x89\\xE5\\x53\\x50\\x48\\x89\\xFB\\x48\\x8B\\x05\\x70\\xC7\\x03\\x00\\x48\\x8B\\x00\\x48\\x89\\x45\\xF0\\xE8\\x24\\xD7\\xFE\\xFF\\x48\\x83\\xC3\\x08\\x48\\x39\\xD8\\x0F|\\x6A\\x01\\x58\\xC3\\x53\\x50\\x48\\x89\\xFB\\x48\\x8B\\x05\\x70\\xC7\\x03\\x00\\x48\\x8B\\x00\\x48\\x89\\x45\\xF0\\xE8\\x24\\xD7\\xFE\\xFF\\x48\\x83\\xC3\\x08\\x48\\x39\\xD8\\x0F|gs' '\(setupPath)'
""",
//
"""
perl -0777pi -e 'BEGIN{$/=\\1e8} s|\\xFF\\xC3\\x00\\xD1\\xF4\\x4F\\x01\\xA9\\xFD\\x7B\\x02\\xA9\\xFD\\x83\\x00\\x91\\xF3\\x03\\x00\\xAA\\x1F\\x20\\x03\\xD5\\x68\\xA1\\x1D\\x58\\x08\\x01\\x40\\xF9\\xE8\\x07\\x00\\xF9|\\x20\\x00\\x80\\xD2\\xC0\\x03\\x5F\\xD6\\xFD\\x7B\\x02\\xA9\\xFD\\x83\\x00\\x91\\xF3\\x03\\x00\\xAA\\x1F\\x20\\x03\\xD5\\x68\\xA1\\x1D\\x58\\x08\\x01\\x40\\xF9\\xE8\\x07\\x00\\xF9|gs' '\(setupPath)'
""",
//
"codesign --remove-signature '\(setupPath)'",
//
"codesign -f -s - --timestamp=none --all-architectures --deep '\(setupPath)'",
//
"xattr -cr '\(setupPath)'"
]

View File

@@ -9,11 +9,15 @@ import Foundation
@objc(HelperToolProtocol) protocol HelperToolProtocol {
func executeCommand(_ command: String, withReply reply: @escaping (String) -> Void)
func startInstallation(_ command: String, withReply reply: @escaping (String) -> Void)
func getInstallationOutput(withReply reply: @escaping (String) -> Void)
}
class HelperTool: NSObject, HelperToolProtocol {
private let listener: NSXPCListener
private var connections: Set<NSXPCConnection> = []
private var currentTask: Process?
private var outputPipe: Pipe?
override init() {
listener = NSXPCListener(machServiceName: "com.x1a0he.macOS.Adobe-Downloader.helper")
@@ -32,7 +36,6 @@ class HelperTool: NSObject, HelperToolProtocol {
func executeCommand(_ command: String, withReply reply: @escaping (String) -> Void) {
print("[Adobe Downloader Helper] 收到执行命令请求: \(command)")
print("[Adobe Downloader Helper] 当前进程权限: \(geteuid())")
let task = Process()
let pipe = Pipe()
@@ -42,27 +45,88 @@ class HelperTool: NSObject, HelperToolProtocol {
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/sh")
currentTask = task
do {
print("[Adobe Downloader Helper] 开始执行命令")
try task.run()
task.waitUntilExit()
let status = task.terminationStatus
print("[Adobe Downloader Helper] 命令执行完成,退出状态: \(status)")
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) {
let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines)
print("[Adobe Downloader Helper] 命令执行成功,输出: \(trimmedOutput)")
reply(trimmedOutput)
reply(output.trimmingCharacters(in: .whitespacesAndNewlines))
} else {
print("[Adobe Downloader Helper] 无法解码命令输出")
reply("Error: Could not decode command output")
}
} catch {
print("[Adobe Downloader Helper] 命令执行失败: \(error)")
reply("Error: \(error.localizedDescription)")
}
currentTask = nil
}
func startInstallation(_ command: String, withReply reply: @escaping (String) -> Void) {
print("[Adobe Downloader Helper] 收到安装请求: \(command)")
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/sh")
currentTask = task
outputPipe = pipe
do {
try task.run()
reply("Started")
} catch {
currentTask = nil
outputPipe = nil
reply("Error: \(error.localizedDescription)")
}
}
func getInstallationOutput(withReply reply: @escaping (String) -> Void) {
guard let pipe = outputPipe else {
reply("")
return
}
let data = pipe.fileHandleForReading.availableData
if data.isEmpty {
if let task = currentTask, !task.isRunning {
currentTask = nil
outputPipe = nil
reply("Completed")
} else {
reply("")
}
return
}
if let output = String(data: data, encoding: .utf8) {
let lines = output.components(separatedBy: .newlines)
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
.joined(separator: "\n")
if !lines.isEmpty {
reply(lines)
} else {
reply("")
}
} else {
reply("")
}
}
func terminateCurrentTask() {
if let pipe = outputPipe {
pipe.fileHandleForReading.readabilityHandler = nil
}
currentTask?.terminate()
currentTask = nil
outputPipe = nil
}
}

View File

@@ -14,16 +14,6 @@
}
}
},
"(退出代码: %d)" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Exit Code: %d)"
}
}
}
},
"/" : {
},
@@ -611,6 +601,21 @@
}
}
}
},
"安装失败 (退出代码: %lld)" : {
},
"安装失败: Setup 组件未被处理 (退出代码: %lld)" : {
},
"安装失败: 安装文件不完整或损坏 (退出代码: %lld)" : {
},
"安装失败: 权限问题 (退出代码: %lld)" : {
},
"安装失败: 架构或版本不一致 (退出代码: %lld)" : {
},
"安装完成" : {
"localizations" : {
@@ -642,16 +647,6 @@
}
}
},
"安装程序调用失败Setup 组件未被处理,请联系开发者" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The installation program failed to call, please contact developer"
}
}
}
},
"尝试使用不同的搜索关键词" : {
"localizations" : {
"en" : {
@@ -1031,16 +1026,6 @@
}
}
},
"正在准备..." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Preparing..."
}
}
}
},
"正在准备安装..." : {
"localizations" : {
"en" : {
@@ -1081,25 +1066,8 @@
}
}
},
"正在安装..." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Installing packages..."
}
}
}
},
"正在重试安装..." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Retrying installation..."
}
}
}
"正在安装..." : {
},
"没有写入权限" : {
"localizations" : {
@@ -1142,17 +1110,6 @@
}
}
},
"目录:" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Directory: "
}
}
}
},
"确定" : {
"localizations" : {
"en" : {
@@ -1165,31 +1122,9 @@
},
"确定要下载并安装 Setup 组件吗?" : {
},
"确定要下载并安装 Setup 组件吗?这个操作需要管理员权限。" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Are you sure you want to download and install the Setup components? This operation requires administrator privileges."
}
}
}
},
"确定要重新处理 Setup 组件吗?" : {
},
"确定要重新处理 Setup 组件吗?这个操作需要管理员权限。" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Are you sure you want to reprocess the Setup components? This operation requires administrator privileges."
}
}
}
},
"确认" : {
"localizations" : {
@@ -1384,17 +1319,6 @@
}
}
},
"语言:" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Language: "
}
}
}
},
"请求超时,请检查网络连接后重试" : {
"comment" : "Network timeout",
"localizations" : {