Files
youle_app_ios_v2/ylgamehall/RootViewController.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

176 lines
8.0 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.
//
// RootViewController.swift
// ylgamehall
//
// M0 阶段临时根控制器,后续将被 Coordinator + LobbyViewController 替换。
//
import UIKit
final class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Phase 1.2 烟雾测试:打印 ChannelConfig.plist 的 11 项渠道注入值
let cfg = BundleConfig.shared
print("""
[BundleConfig] 渠道注入读取结果:
qiniuDomain = \(cfg.qiniuDomain)
gameId = \(cfg.gameId)
channel = \(cfg.channel)
gameDir = \(cfg.gameDir)
gameStart = \(cfg.gameStart)
gameConfig = \(cfg.gameConfig)
market = \(cfg.market)
agent = \(cfg.agent)
appVersion = \(cfg.appVersion)
other = "\(cfg.other)"
appleConfig = "\(cfg.appleConfig)"
""")
// Phase 1.3 烟雾测试:打印 SandboxPaths 计算路径
print("""
[SandboxPaths] 路径计算结果:
caches = \(SandboxPaths.caches.path)
documents = \(SandboxPaths.documents.path)
bundle = \(SandboxPaths.bundle.path)
lobbyRoot = \(SandboxPaths.lobbyRoot.path)
lobbyIndex = \(SandboxPaths.lobbyIndex.path)
lobbyVersionXML = \(SandboxPaths.lobbyVersionXML.path)
""")
// Phase 1.5 烟雾测试:触发 ResourceUnzipper,确认 lobby index.html 存在
Task {
let start = Date()
do {
try await ResourceUnzipper.shared.ensureReady()
let elapsed = Date().timeIntervalSince(start)
let exists = FileManager.default.fileExists(atPath: SandboxPaths.lobbyIndex.path)
print("""
[ResourceUnzipper] ensureReady 完成(耗时 \(String(format: "%.3f", elapsed))s
lobbyIndex 存在: \(exists)
""")
} catch {
print("[ResourceUnzipper] ERROR: \(error)")
}
}
// Phase 1.8 烟雾测试:确认 WebViewJavascriptBridge.js 已入 Bundle 且可读
// 注:synchronized group 会把 ylgamehall/Resources/JS/ 子目录扁平化为 bundle 根,
// 所以无需 subdirectory 参数。
if let url = Bundle.main.url(forResource: "WebViewJavascriptBridge",
withExtension: "js"),
let content = try? String(contentsOf: url, encoding: .utf8) {
print("""
[WVJB.js] 已就绪
path = \(url.path)
bytes = \(content.utf8.count)
head = \(content.prefix(60).replacingOccurrences(of: "\n", with: " ⏎ "))
""")
} else {
print("[WVJB.js] ERROR: 找不到 Bundle 内 JS/WebViewJavascriptBridge.js")
}
// Phase 1.10 烟雾测试:拉远端渠道配置 .txt(含 cache-busting + 短文本响应识别)
Task {
let start = Date()
do {
let outcome = try await RemoteConfigClient.shared.fetch()
let elapsed = Date().timeIntervalSince(start)
switch outcome {
case .parsed(let cfg):
print("""
[RemoteConfigClient] 拉取成功 .parsed(耗时 \(String(format: "%.3f", elapsed))s
showmessage = \(cfg.showmessage ?? "<nil>")
agentlist count = \(cfg.agentlist?.count ?? 0)
""")
if let first = cfg.agentlist?.first {
print("""
agent[0].agentid = \(first.agentid ?? "<nil>")
agent[0].appVersion = \(first.appVersion ?? "<nil>")
agent[0].gameVersion = \(first.gameVersion ?? "<nil>")
agent[0].channellist = \(first.channellist?.count ?? 0)
agent[0].gamelist = \(first.gamelist?.count ?? 0)
""")
}
// Phase 1.11 烟雾测试:chulishengji 双子树合并
let bc = BundleConfig.shared
let resolved = VersionResolver.resolve(
config: cfg,
agentId: bc.agent,
channelId: bc.channel,
marketId: bc.market,
gameId: bc.gameId
)
print("""
[VersionResolver] chulishengji 双子树合并结果:
appVersion = \(resolved.appVersion)
appDownload = \(resolved.appDownload ?? "<nil>")
gameVersion = \(resolved.gameVersion)
gameZip = \(resolved.gameZip ?? "<nil>")
showmessage = "\(resolved.showmessage ?? "<nil>")"
""")
// Phase 1.12 烟雾测试:读本地版本号 + 与远端对比
let localApp = LocalVersionReader.localAppVersion
let localGame = LocalVersionReader.localGameVersion
let needIPAUpgrade = resolved.appVersion > localApp
let needZipUpgrade = resolved.gameVersion > localGame
print("""
[LocalVersionReader] 本地 vs 远端对比:
本地 appVersion = \(localApp)(来自 ChannelConfig.plist
远端 appVersion = \(resolved.appVersion)
→ IPA 升级 = \(needIPAUpgrade ? "需要" : "不需要")
本地 gameVersion = \(localGame)(来自 version.xml /game/version@value
远端 gameVersion = \(resolved.gameVersion)
→ 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):
print("""
[RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s
运营杀手锏:应当弹窗显示并停止重试
内容 = "\(msg)"
""")
}
} catch {
print("[RemoteConfigClient] ERROR: \(error)")
}
}
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
.landscape
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
.landscapeRight
}
override var prefersStatusBarHidden: Bool { false }
}