From 9f4ccb762edaf335a48f6eb95c8089eb40196c93 Mon Sep 17 00:00:00 2001 From: joywayer Date: Mon, 22 Jun 2026 08:09:57 +0800 Subject: [PATCH] =?UTF-8?q?Design=20=C2=A77.5.3/.8=EF=BC=9AAppDataWriter?= =?UTF-8?q?=20=E5=8A=A0=20os.log=20=E8=B0=83=E8=AF=95=E8=BE=93=E5=87=BA=20?= =?UTF-8?q?+=20=E8=81=94=E8=B0=83=E5=8F=AF=E8=A7=82=E5=AF=9F=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户在 Phase 1.17 联调准备时反馈:app_*.js 写入时应该有控制台输出, 方便调试。本 commit 在 §7.5 蓝图层落实可观察性,Phase 2 实施按此骨架 就有完整 debug log。 §7.5.3 AppDataWriter 改动(同时修正若干与 msext 字面不一致的细节): - 引入 os.Logger(subsystem: ylgamehall, category: AppDataWriter) - writeAppData / writeGamesName / writeBattery / writeNetwork 全部打印 {ContainerRole} + 文件路径 + 每个 key=value 一行,便于和 H5 console.log(app_xxx) 逐项对账 - ContainerRole 实现 CustomStringConvertible("lobby" / "subGame(name)") - Logger.debug 级别:Debug 出 Xcode 控制台,Release 自动过滤 - privacy: .public 标注(确保字符串值不被 Logger 模糊化为 ) **字面修正**(对照 daoqi/msext 实际代码二次校对): - 字符串包裹符 " → '(msext 用 "var x='%@';" 单引号) - escape 转义对象从 \" 改为 \'(与单引号包裹配套) - 数值字段(version / Launchtype / getwifisignalLevel / appversion) 去掉引号,沿用 msext "var app_version=1;" / "var app_appversion='%d';" - app_gamesname 从 JS array literal [...] 改为 new Array(...), var 后两空格,严格 AppDelegate.m:259 字面 §7.5.8 新增「联调时的可观察性(Phase 1.17 调试手段)」: - 路径 1:Xcode 控制台 — 原生 os.log 输出格式样例 - 路径 2:沙盒文件直接 cat — xcrun simctl get_app_container 命令 + 4 个 .js 文件 cat 验证 - 路径 3:H5 console — Safari Web Inspector 逐项 console.log(app_xxx) 样例,标注 ⚠️ 大小写硬约束(Launchtype L 大写 / battery 带 get 前缀) - 路径 4:真机 Safari 调试启用方法 - Release 注意事项:os.log .debug 自动过滤避免性能开销和敏感字段外泄 Co-Authored-By: Claude Opus 4.7 --- docs/H5-Native-Implementation-Design.md | 161 +++++++++++++++++++----- 1 file changed, 129 insertions(+), 32 deletions(-) diff --git a/docs/H5-Native-Implementation-Design.md b/docs/H5-Native-Implementation-Design.md index ae94c4a..907d229 100644 --- a/docs/H5-Native-Implementation-Design.md +++ b/docs/H5-Native-Implementation-Design.md @@ -2007,8 +2007,12 @@ done #### 7.5.3 Swift 骨架 +设计带可观察性:每次写文件都在 Debug 模式下把"写了哪个文件 + 内容预览 + 路径"打到控制台,便于联调时一眼对账 H5 端读到的值。Release 自动去 `.debug` 级别避免性能开销 + 防泄漏。 + ```swift // Source/WebView/AppDataWriter.swift +import os.log + @MainActor public struct AppDataWriter { @@ -2016,13 +2020,22 @@ public struct AppDataWriter { let resolvedVersion: ResolvedVersion? // 拉到远端后传入;首次 fallback nil let containerRole: ContainerRole // .lobby / .subGame(name:) - public enum ContainerRole { + public enum ContainerRole: CustomStringConvertible { case lobby case subGame(name: String, dir: String) + + public var description: String { + switch self { + case .lobby: return "lobby" + case .subGame(let n, _): return "subGame(\(n))" + } + } } - /// 一次性写 #1-#12 到 app_data.js(loadFileURL 前调一次) - /// 同时写 app_gamesname.js(#13)。 + /// 集中日志入口:可被单测重定向;Debug 输出,Release 自动归档 + private static let log = Logger(subsystem: "ylgamehall", category: "AppDataWriter") + + /// 一次性写 #1-#12 到 app_data.js + #13 app_gamesname.js(loadFileURL 前调一次) public func writeInitial() throws { try writeAppData() try writeGamesName() @@ -2030,16 +2043,19 @@ public struct AppDataWriter { /// 写 #14 app_battery.js(电池变化 / viewWillAppear 触发) public func writeBattery(_ level: Float) throws { - let line = "var app_getbattery = \"\(String(format: "%.2f", level))\";\n" - try line.write(to: filePath("app_battery.js"), - atomically: true, encoding: .utf8) + let value = String(format: "%.2f", level) + let line = "var app_getbattery=\(value);\n" + let url = filePath("app_battery.js") + try line.write(to: url, atomically: true, encoding: .utf8) + Self.log.debug("[\(containerRole.description, privacy: .public)] write app_battery.js: app_getbattery=\(value, privacy: .public) → \(url.path, privacy: .public)") } /// 写 #15 app_network.js(NWPath 变化 / viewWillAppear 触发) public func writeNetwork(_ code: Int) throws { - let line = "var app_getnetwork = \"\(code)\";\n" - try line.write(to: filePath("app_network.js"), - atomically: true, encoding: .utf8) + let line = "var app_getnetwork=\(code);\n" + let url = filePath("app_network.js") + try line.write(to: url, atomically: true, encoding: .utf8) + Self.log.debug("[\(containerRole.description, privacy: .public)] write app_network.js: app_getnetwork=\(code, privacy: .public) → \(url.path, privacy: .public)") } private func writeAppData() throws { @@ -2064,33 +2080,53 @@ public struct AppDataWriter { let appVersion = resolvedVersion?.appVersion.description ?? bc.appVersion - // 注意:所有 var 都是 string 字面量(JS 端用 == 比较,不区分类型, - // 但 msext 沿用 string);大小写严格保持,不能改名 + // ⚠️ 严格保持:单引号包裹字符串、msext 沿用形式;大小写不能改 + // 数值字段(app_version / app_Launchtype / app_getwifisignalLevel / app_appversion) + // 是字面数字,无引号;msext "var app_version=1;" / "var app_appversion='%d';" let lines = """ - var app_version = "1"; - var app_gameconfig = "\(escape(bc.gameConfig))"; - var app_gamedir = "\(escape(gameDir))"; - var app_gamestart = "\(escape(gameStart))"; - var app_agent = "\(escape(bc.agent))"; - var app_appversion = "\(escape(appVersion))"; - var app_market = "\(escape(bc.market))"; - var app_channel = "\(escape(bc.channel))"; - var app_Launchtype = "\(launchtype)"; - var app_getwifisignalLevel= "1"; - var app_gamename = "\(escape(gameName))"; - var app_invitationcode = "\(escape(bc.other))"; + var app_version=1;\ + var app_gameconfig='\(escape(bc.gameConfig))';\ + var app_gamedir='\(escape(gameDir))';\ + var app_gamestart='\(escape(gameStart))';\ + var app_agent='\(escape(bc.agent))';\ + var app_appversion='\(escape(appVersion))';\ + var app_market='\(escape(bc.market))';\ + var app_channel='\(escape(bc.channel))';\ + var app_Launchtype=\(launchtype);\ + var app_getwifisignalLevel=1;\ + var app_gamename='\(escape(gameName))';\ + var app_invitationcode='\(escape(bc.other))'; """ - try lines.write(to: filePath("app_data.js"), - atomically: true, encoding: .utf8) + let url = filePath("app_data.js") + try lines.write(to: url, atomically: true, encoding: .utf8) + + // Debug 打印每个 key=value 一行,便于和 H5 console.log(app_xxx) 逐项对账 + Self.log.debug(""" + [\(containerRole.description, privacy: .public)] write app_data.js → \(url.path, privacy: .public) + app_version = 1 + app_gameconfig = '\(bc.gameConfig, privacy: .public)' + app_gamedir = '\(gameDir, privacy: .public)' + app_gamestart = '\(gameStart, privacy: .public)' + app_agent = '\(bc.agent, privacy: .public)' + app_appversion = '\(appVersion, privacy: .public)' + app_market = '\(bc.market, privacy: .public)' + app_channel = '\(bc.channel, privacy: .public)' + app_Launchtype = \(launchtype, privacy: .public) + app_getwifisignalLevel = 1 + app_gamename = '\(gameName, privacy: .public)' + app_invitationcode = '\(bc.other, privacy: .public)' + """) } private func writeGamesName() throws { - // JS Array literal: ["game1","game2",...] + // JS Array literal: new Array('game1','game2',...),与 msext AppDelegate.m:259 完全一致 let installed = SandboxPaths.installedSubGameNames() - let escaped = installed.map { "\"\(escape($0))\"" }.joined(separator: ",") - let line = "var app_gamesname = [\(escaped)];\n" - try line.write(to: filePath("app_gamesname.js"), - atomically: true, encoding: .utf8) + let escaped = installed.map { "'\(escape($0))'" }.joined(separator: ",") + let line = "var app_gamesname=new Array(\(escaped));\n" + // ^ 注意 var 后两个空格,msext 字面沿用 + let url = filePath("app_gamesname.js") + try line.write(to: url, atomically: true, encoding: .utf8) + Self.log.debug("[\(containerRole.description, privacy: .public)] write app_gamesname.js: \(installed.count, privacy: .public) games → \(url.path, privacy: .public)\n games = \(installed, privacy: .public)") } private func filePath(_ name: String) -> URL { @@ -2105,10 +2141,11 @@ public struct AppDataWriter { return base.appendingPathComponent(name) } - /// JS string 转义:双引号、反斜杠、换行 + /// JS string 转义:单引号、反斜杠、换行(注意 msext 用单引号包裹字符串, + /// 转义对象是单引号不是双引号) private func escape(_ s: String) -> String { s.replacingOccurrences(of: "\\", with: "\\\\") - .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "'", with: "\\'") .replacingOccurrences(of: "\n", with: "\\n") .replacingOccurrences(of: "\r", with: "\\r") } @@ -2185,6 +2222,66 @@ private func runBootPipelineSteps() async throws { | §3.4 OverlayBridge polyfill | ✓ **保留** | 弹层 `window.settings.{backgameData/browser/finishweb}`,与系统版本无关 | | §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 文件重写互为补充 | +#### 7.5.8 联调时的可观察性(Phase 1.17 调试手段) + +H5 端读到 undefined / 值不对时,第一步是确认"原生写了什么、H5 读到什么"。三条对账路径: + +**1. Xcode 控制台 — 原生写入 log(§7.5.3 已埋点)** + +启动后 Xcode console 应出现: + +``` +[lobby] write app_data.js → /Users/.../Caches///app_data.js + app_version = 1 + app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test' + app_gamedir = 'FtJf07...' + app_gamestart = 'gamehall' + ... + app_Launchtype = 0 + app_getwifisignalLevel = 1 + app_gamename = 'gamehall' + app_invitationcode = '' +[lobby] write app_gamesname.js: 0 games → /Users/.../Caches///app_gamesname.js + games = [] +[lobby] write app_battery.js: app_getbattery=0.85 → ... +[lobby] write app_network.js: app_getnetwork=2 → ... +``` + +**2. 沙盒文件直接读 — 等价 `cat`** + +如果控制台 log 没问题但 H5 端仍读 undefined,去沙盒拿实际文件验证写入和"H5 看到的"是否一致: + +```bash +# 模拟器路径 +xcrun simctl get_app_container booted com.skyapp.ylgamehall data +# 拿到容器路径后 +cat <容器>/Library/Caches///app_data.js +cat <容器>/Library/Caches///app_battery.js +cat <容器>/Library/Caches///app_network.js +cat <容器>/Library/Caches///app_gamesname.js +``` + +文件应包含 `var app_xxx=...;` 字面字符串。若文件不存在或为空 → AppDataWriter 没跑到 / 路径错;若文件正确但 H5 读 undefined → H5 端 `