Files
youle_app_ios_v2/ylgamehall/Source/Resource/LobbyZipUpgrader.swift
T
joywayer 0887088bb8 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 渲染出来。
2026-06-22 02:27:48 +08:00

102 lines
3.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 MB4G 网络约 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. 下载到 tmpURLSession 默认管理临时位置)
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)
}
}