- 跳过已撤回的版本时不再显示"有更新";安装前发现更新源换了版本时给出明确提示(补齐 7 种语言) - 补充双区域回退与失败重排的调度测试 - 回收结果按标准化路径比对,避免路径写法不同误报本体未移动 - 抽出 scan 统一后台遍历与取消,loadApps 嵌套降到 3 层 - 自我保护测试只校验错误语义,不再比较两次翻译 - sign_sparkle.sh 在证书不在钥匙串有效列表时提前报错,避免子组件漏签 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
3.2 KiB
Swift
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)")
|
|
}
|
|
}
|
|
}
|
|
}
|