Files
ysicingandClaude Opus 5.5 f1dd8cfacf fix: 收尾 PR 审查遗留的更新、卸载与签名问题
- 跳过已撤回的版本时不再显示"有更新";安装前发现更新源换了版本时给出明确提示(补齐 7 种语言)
- 补充双区域回退与失败重排的调度测试
- 回收结果按标准化路径比对,避免路径写法不同误报本体未移动
- 抽出 scan 统一后台遍历与取消,loadApps 嵌套降到 3 层
- 自我保护测试只校验错误语义,不再比较两次翻译
- sign_sparkle.sh 在证书不在钥匙串有效列表时提前报错,避免子组件漏签

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:02 +08:00

68 lines
3.2 KiB
Swift

// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Darwin
import Foundation
import Localization
import Security
public enum UpdateError: Error, LocalizedError, Equatable {
case download(String)
case invalidBundle(String)
case install(String)
/// 确认后到开始安装之间,更新源已换成另一个版本。
case releaseChanged
public var errorDescription: String? {
switch self {
case .download(let reason): tr("下载失败:\(reason)")
case .invalidBundle(let reason): tr("安装包内容不正确:\(reason)")
case .install(let reason): tr("替换应用失败:\(reason)")
case .releaseChanged: tr("更新源已发布其他版本,请重新检查更新后再安装")
}
}
}
/// 本机安装能力与 Widget 升级协调;下载、验证、替换和重启由 Sparkle 负责。
public enum UpdateInstaller {
/// 应用自身的签名团队;开发构建(未签名或临时签名)返回 nil
public static func teamIdentifier(of app: URL) -> String? {
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(app as CFURL, [], &staticCode) == errSecSuccess, let code = staticCode else { return nil }
var info: CFDictionary?
guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &info) == errSecSuccess,
let dictionary = info as? [String: Any] else { return nil }
return dictionary[kSecCodeInfoTeamIdentifier as String] as? String
}
/// 仅结束属于这份应用的旧小组件进程,避免替换后 WidgetKit 继续向旧进程索取组件清单。
public static func stopWidgetExtension(in app: URL) throws {
let executable = app.appendingPathComponent("Contents/PlugIns/XStatsWidget.appex/Contents/MacOS/XStatsWidget").path
var resolved = [CChar](repeating: 0, count: 4096)
guard realpath(executable, &resolved) != nil else { return }
let expectedPath = String(decoding: resolved.prefix(while: { $0 != 0 }).map { UInt8(bitPattern: $0) }, as: UTF8.self)
let capacity = max(1, Int(proc_listallpids(nil, 0)) + 128)
var pids = [pid_t](repeating: 0, count: capacity)
let count = pids.withUnsafeMutableBytes { proc_listallpids($0.baseAddress, Int32($0.count)) }
guard count >= 0 else { throw UpdateError.install(tr("应用正在运行,请先退出")) }
for pid in pids.prefix(Int(count)) where pid > 0 {
var path = [CChar](repeating: 0, count: 4096)
guard proc_pidpath(pid, &path, UInt32(path.count)) > 0,
String(decoding: path.prefix(while: { $0 != 0 }).map { UInt8(bitPattern: $0) }, as: UTF8.self) == expectedPath else {
continue
}
if kill(pid, SIGTERM) != 0 && errno != ESRCH {
throw UpdateError.install(tr("应用正在运行,请先退出") + " (XStatsWidget)")
}
for _ in 0..<50 {
if kill(pid, 0) != 0 { break }
usleep(100_000)
}
if kill(pid, 0) == 0 {
throw UpdateError.install(tr("应用正在运行,请先退出") + " (XStatsWidget)")
}
}
}
}