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 渲染出来。
176 lines
8.0 KiB
Swift
176 lines
8.0 KiB
Swift
//
|
||
// 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 }
|
||
}
|