Phase 1.13:LobbyZipUpgrader 完成 H5 zip 升级端到端链路
Phase 1 远程配置链路(1.10-1.13)最后一环:把 VersionResolver 解出的
gameZip URL 下载 + 解压 + 原子 rename 到 lobbyRoot,触发条件是远端
gameVersion > 本地 version.xml /game/version@value。
- 新增 ylgamehall/Source/Resource/LobbyZipUpgrader.swift
- public actor,单例 LobbyZipUpgrader.shared
- upgradeIfNeeded(resolved:) async throws -> Outcome
- .noop:远端版本 ≤ 本地,或 gameZip nil / 非法 URL,直接返回
- .upgraded(from:to:):完成升级,附前后版本号
- URLSession.download(from:) 异步下载(60s 请求 / 300s 资源超时),
HTTP 状态码非 2xx 抛 .httpStatus
- 解压到 caches/lobby-staging-{UUID}/ 隔离目录(不直接覆盖 lobbyRoot,
msext "removeItem + ZipArchive overWrite:YES" 半途崩溃会留半残;
我们 staging 完整解压成功后才 rename)
- 原子 rename:fm.removeItem(lobbyRoot) → fm.moveItem(staging,
to: lobbyRoot);任一步失败抛 .stagingMoveFailed,调用方决定回退
- 类型化 UpgradeError 5 种:badGameZipURL / downloadFailed /
httpStatus / unzipFailed / stagingMoveFailed
- 实现照搬 msext NewRootVC.m:1789-1869 downFileFromServer + unzip
+ unzipDone 的语义,但用现代 async + 原子 rename 替代 ASIHTTPRequest
+ detachNewThreadSelector + ZipArchive overWrite:YES
- RootViewController 烟雾测试串接 LobbyZipUpgrader:
调 upgradeIfNeeded → switch outcome → 升级后重新读 version.xml 验证
- 真机实测端到端链路:
第一次:本地 260 vs 远端 261 → 升级完成 261,耗时 2.007s
(下载 11.5 MB HTTP zip + ZIPFoundation 解压 + 原子 rename)
重新读 version.xml = 261 ✓ lobbyIndex 仍存在 ✓
第二次:本地 261 vs 远端 261 → .noop,耗时 0.001s(幂等验证)
Phase 1 远程配置链路(1.10-1.13)完整闭环。下一步 Phase 1.14
WebContainer 把这条链路插到 viewDidLoad 之前 + 16:9 letterbox 加载
lobbyIndex,第一次让真实大厅 H5 渲染出来。
This commit is contained in:
@@ -128,6 +128,28 @@ final class RootViewController: UIViewController {
|
|||||||
远端 gameVersion = \(resolved.gameVersion)
|
远端 gameVersion = \(resolved.gameVersion)
|
||||||
→ H5 zip 升级 = \(needZipUpgrade ? "需要" : "不需要")
|
→ H5 zip 升级 = \(needZipUpgrade ? "需要" : "不需要")
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
// Phase 1.13 烟雾测试:触发 LobbyZipUpgrader(如果需要升级)
|
||||||
|
let upgradeStart = Date()
|
||||||
|
do {
|
||||||
|
let outcome = try await LobbyZipUpgrader.shared.upgradeIfNeeded(resolved: resolved)
|
||||||
|
let elapsed = Date().timeIntervalSince(upgradeStart)
|
||||||
|
switch outcome {
|
||||||
|
case .noop:
|
||||||
|
print("""
|
||||||
|
[LobbyZipUpgrader] 无需升级,跳过(耗时 \(String(format: "%.3f", elapsed))s)
|
||||||
|
""")
|
||||||
|
case .upgraded(let from, let to):
|
||||||
|
let newLocalGame = LocalVersionReader.localGameVersion
|
||||||
|
print("""
|
||||||
|
[LobbyZipUpgrader] 升级完成 \(from) → \(to)(耗时 \(String(format: "%.3f", elapsed))s)
|
||||||
|
重新读 version.xml: localGameVersion = \(newLocalGame)
|
||||||
|
lobbyIndex 存在: \(FileManager.default.fileExists(atPath: SandboxPaths.lobbyIndex.path))
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[LobbyZipUpgrader] ERROR: \(error)")
|
||||||
|
}
|
||||||
case .shortText(let msg):
|
case .shortText(let msg):
|
||||||
print("""
|
print("""
|
||||||
[RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s)
|
[RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s)
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// LobbyZipUpgrader.swift
|
||||||
|
// ylgamehall
|
||||||
|
//
|
||||||
|
// H5 zip 升级:下载远端 game_zip → 临时目录解压 → 原子 rename 到 lobbyRoot。
|
||||||
|
// 详见 docs/Development-Plan.md ADR-008-G / Design §6.3.4 / msext NewRootVC.m:1789-1869。
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ZIPFoundation
|
||||||
|
|
||||||
|
public actor LobbyZipUpgrader {
|
||||||
|
|
||||||
|
public static let shared = LobbyZipUpgrader()
|
||||||
|
|
||||||
|
public enum Outcome: Sendable {
|
||||||
|
/// 本地版本已是最新(含远端 gameZip 为空 / 版本号未提升的情况),未做任何下载
|
||||||
|
case noop
|
||||||
|
/// 完成升级。`from → to` 是版本号变化。
|
||||||
|
case upgraded(from: Int, to: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UpgradeError: Error, Sendable {
|
||||||
|
case badGameZipURL(String)
|
||||||
|
case downloadFailed(any Error)
|
||||||
|
case httpStatus(Int)
|
||||||
|
case unzipFailed(any Error)
|
||||||
|
case stagingMoveFailed(any Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let session: URLSession
|
||||||
|
|
||||||
|
public init(session: URLSession = LobbyZipUpgrader.defaultSession) {
|
||||||
|
self.session = session
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 默认 URLSession:下载用 60s 超时(zip 大约 11 MB,4G 网络约 10s)。
|
||||||
|
nonisolated public static var defaultSession: URLSession {
|
||||||
|
let cfg = URLSessionConfiguration.default
|
||||||
|
cfg.timeoutIntervalForRequest = 60
|
||||||
|
cfg.timeoutIntervalForResource = 300
|
||||||
|
return URLSession(configuration: cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 比较远端 game_version 与本地 version.xml。需要升级则下载 + 解压 + 原子 rename。
|
||||||
|
///
|
||||||
|
/// - Parameter resolved: VersionResolver.resolve(...) 的结果
|
||||||
|
/// - Returns: `.noop` 已是最新;`.upgraded(from:to:)` 完成升级
|
||||||
|
public func upgradeIfNeeded(resolved: ResolvedVersion) async throws -> Outcome {
|
||||||
|
let localGameVersion = LocalVersionReader.localGameVersion
|
||||||
|
guard resolved.gameVersion > localGameVersion,
|
||||||
|
let zipURLString = resolved.gameZip,
|
||||||
|
let zipURL = URL(string: zipURLString)
|
||||||
|
else {
|
||||||
|
return .noop
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 下载到 tmp(URLSession 默认管理临时位置)
|
||||||
|
let downloadedURL: URL
|
||||||
|
let response: URLResponse
|
||||||
|
do {
|
||||||
|
(downloadedURL, response) = try await session.download(from: zipURL)
|
||||||
|
} catch {
|
||||||
|
throw UpgradeError.downloadFailed(error)
|
||||||
|
}
|
||||||
|
if let http = response as? HTTPURLResponse,
|
||||||
|
!(200..<300).contains(http.statusCode) {
|
||||||
|
throw UpgradeError.httpStatus(http.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解压到隔离 staging 目录
|
||||||
|
let staging = SandboxPaths.caches
|
||||||
|
.appendingPathComponent("lobby-staging-\(UUID().uuidString)")
|
||||||
|
let fm = FileManager.default
|
||||||
|
do {
|
||||||
|
try fm.createDirectory(at: staging, withIntermediateDirectories: true)
|
||||||
|
try fm.unzipItem(at: downloadedURL, to: staging)
|
||||||
|
} catch {
|
||||||
|
try? fm.removeItem(at: staging)
|
||||||
|
throw UpgradeError.unzipFailed(error)
|
||||||
|
}
|
||||||
|
try? fm.removeItem(at: downloadedURL)
|
||||||
|
|
||||||
|
// 3. 原子 rename:删旧 lobbyRoot → 把 staging 移到位
|
||||||
|
// (msext "removeItem + ZipArchive overWrite:YES" 半途崩溃留半残;
|
||||||
|
// 我们 staging 完整解压后再 rename,半途崩溃只留 staging,下次启动可清)
|
||||||
|
let lobbyRoot = SandboxPaths.lobbyRoot
|
||||||
|
do {
|
||||||
|
if fm.fileExists(atPath: lobbyRoot.path) {
|
||||||
|
try fm.removeItem(at: lobbyRoot)
|
||||||
|
}
|
||||||
|
// 父目录确保存在(Library/Caches 永远存在,无需创建中间路径)
|
||||||
|
try fm.moveItem(at: staging, to: lobbyRoot)
|
||||||
|
} catch {
|
||||||
|
try? fm.removeItem(at: staging)
|
||||||
|
throw UpgradeError.stagingMoveFailed(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .upgraded(from: localGameVersion, to: resolved.gameVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user