Design §7.5.3/.8:AppDataWriter 加 os.log 调试输出 + 联调可观察性
用户在 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 模糊化为 <private>)
**字面修正**(对照 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0cdd709e66
commit
9f4ccb762e
@@ -2007,8 +2007,12 @@ done
|
|||||||
|
|
||||||
#### 7.5.3 Swift 骨架
|
#### 7.5.3 Swift 骨架
|
||||||
|
|
||||||
|
设计带可观察性:每次写文件都在 Debug 模式下把"写了哪个文件 + 内容预览 + 路径"打到控制台,便于联调时一眼对账 H5 端读到的值。Release 自动去 `.debug` 级别避免性能开销 + 防泄漏。
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Source/WebView/AppDataWriter.swift
|
// Source/WebView/AppDataWriter.swift
|
||||||
|
import os.log
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct AppDataWriter {
|
public struct AppDataWriter {
|
||||||
|
|
||||||
@@ -2016,13 +2020,22 @@ public struct AppDataWriter {
|
|||||||
let resolvedVersion: ResolvedVersion? // 拉到远端后传入;首次 fallback nil
|
let resolvedVersion: ResolvedVersion? // 拉到远端后传入;首次 fallback nil
|
||||||
let containerRole: ContainerRole // .lobby / .subGame(name:)
|
let containerRole: ContainerRole // .lobby / .subGame(name:)
|
||||||
|
|
||||||
public enum ContainerRole {
|
public enum ContainerRole: CustomStringConvertible {
|
||||||
case lobby
|
case lobby
|
||||||
case subGame(name: String, dir: String)
|
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 前调一次)
|
/// 集中日志入口:可被单测重定向;Debug 输出,Release 自动归档
|
||||||
/// 同时写 app_gamesname.js(#13)。
|
private static let log = Logger(subsystem: "ylgamehall", category: "AppDataWriter")
|
||||||
|
|
||||||
|
/// 一次性写 #1-#12 到 app_data.js + #13 app_gamesname.js(loadFileURL 前调一次)
|
||||||
public func writeInitial() throws {
|
public func writeInitial() throws {
|
||||||
try writeAppData()
|
try writeAppData()
|
||||||
try writeGamesName()
|
try writeGamesName()
|
||||||
@@ -2030,16 +2043,19 @@ public struct AppDataWriter {
|
|||||||
|
|
||||||
/// 写 #14 app_battery.js(电池变化 / viewWillAppear 触发)
|
/// 写 #14 app_battery.js(电池变化 / viewWillAppear 触发)
|
||||||
public func writeBattery(_ level: Float) throws {
|
public func writeBattery(_ level: Float) throws {
|
||||||
let line = "var app_getbattery = \"\(String(format: "%.2f", level))\";\n"
|
let value = String(format: "%.2f", level)
|
||||||
try line.write(to: filePath("app_battery.js"),
|
let line = "var app_getbattery=\(value);\n"
|
||||||
atomically: true, encoding: .utf8)
|
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 触发)
|
/// 写 #15 app_network.js(NWPath 变化 / viewWillAppear 触发)
|
||||||
public func writeNetwork(_ code: Int) throws {
|
public func writeNetwork(_ code: Int) throws {
|
||||||
let line = "var app_getnetwork = \"\(code)\";\n"
|
let line = "var app_getnetwork=\(code);\n"
|
||||||
try line.write(to: filePath("app_network.js"),
|
let url = filePath("app_network.js")
|
||||||
atomically: true, encoding: .utf8)
|
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 {
|
private func writeAppData() throws {
|
||||||
@@ -2064,33 +2080,53 @@ public struct AppDataWriter {
|
|||||||
|
|
||||||
let appVersion = resolvedVersion?.appVersion.description ?? bc.appVersion
|
let appVersion = resolvedVersion?.appVersion.description ?? bc.appVersion
|
||||||
|
|
||||||
// 注意:所有 var 都是 string 字面量(JS 端用 == 比较,不区分类型,
|
// ⚠️ 严格保持:单引号包裹字符串、msext 沿用形式;大小写不能改
|
||||||
// 但 msext 沿用 string);大小写严格保持,不能改名
|
// 数值字段(app_version / app_Launchtype / app_getwifisignalLevel / app_appversion)
|
||||||
|
// 是字面数字,无引号;msext "var app_version=1;" / "var app_appversion='%d';"
|
||||||
let lines = """
|
let lines = """
|
||||||
var app_version = "1";
|
var app_version=1;\
|
||||||
var app_gameconfig = "\(escape(bc.gameConfig))";
|
var app_gameconfig='\(escape(bc.gameConfig))';\
|
||||||
var app_gamedir = "\(escape(gameDir))";
|
var app_gamedir='\(escape(gameDir))';\
|
||||||
var app_gamestart = "\(escape(gameStart))";
|
var app_gamestart='\(escape(gameStart))';\
|
||||||
var app_agent = "\(escape(bc.agent))";
|
var app_agent='\(escape(bc.agent))';\
|
||||||
var app_appversion = "\(escape(appVersion))";
|
var app_appversion='\(escape(appVersion))';\
|
||||||
var app_market = "\(escape(bc.market))";
|
var app_market='\(escape(bc.market))';\
|
||||||
var app_channel = "\(escape(bc.channel))";
|
var app_channel='\(escape(bc.channel))';\
|
||||||
var app_Launchtype = "\(launchtype)";
|
var app_Launchtype=\(launchtype);\
|
||||||
var app_getwifisignalLevel= "1";
|
var app_getwifisignalLevel=1;\
|
||||||
var app_gamename = "\(escape(gameName))";
|
var app_gamename='\(escape(gameName))';\
|
||||||
var app_invitationcode = "\(escape(bc.other))";
|
var app_invitationcode='\(escape(bc.other))';
|
||||||
"""
|
"""
|
||||||
try lines.write(to: filePath("app_data.js"),
|
let url = filePath("app_data.js")
|
||||||
atomically: true, encoding: .utf8)
|
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 {
|
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 installed = SandboxPaths.installedSubGameNames()
|
||||||
let escaped = installed.map { "\"\(escape($0))\"" }.joined(separator: ",")
|
let escaped = installed.map { "'\(escape($0))'" }.joined(separator: ",")
|
||||||
let line = "var app_gamesname = [\(escaped)];\n"
|
let line = "var app_gamesname=new Array(\(escaped));\n"
|
||||||
try line.write(to: filePath("app_gamesname.js"),
|
// ^ 注意 var 后两个空格,msext 字面沿用
|
||||||
atomically: true, encoding: .utf8)
|
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 {
|
private func filePath(_ name: String) -> URL {
|
||||||
@@ -2105,10 +2141,11 @@ public struct AppDataWriter {
|
|||||||
return base.appendingPathComponent(name)
|
return base.appendingPathComponent(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JS string 转义:双引号、反斜杠、换行
|
/// JS string 转义:单引号、反斜杠、换行(注意 msext 用单引号包裹字符串,
|
||||||
|
/// 转义对象是单引号不是双引号)
|
||||||
private func escape(_ s: String) -> String {
|
private func escape(_ s: String) -> String {
|
||||||
s.replacingOccurrences(of: "\\", with: "\\\\")
|
s.replacingOccurrences(of: "\\", with: "\\\\")
|
||||||
.replacingOccurrences(of: "\"", with: "\\\"")
|
.replacingOccurrences(of: "'", with: "\\'")
|
||||||
.replacingOccurrences(of: "\n", with: "\\n")
|
.replacingOccurrences(of: "\n", with: "\\n")
|
||||||
.replacingOccurrences(of: "\r", with: "\\r")
|
.replacingOccurrences(of: "\r", with: "\\r")
|
||||||
}
|
}
|
||||||
@@ -2185,6 +2222,66 @@ private func runBootPipelineSteps() async throws {
|
|||||||
| §3.4 OverlayBridge polyfill | ✓ **保留** | 弹层 `window.settings.{backgameData/browser/finishweb}`,与系统版本无关 |
|
| §3.4 OverlayBridge polyfill | ✓ **保留** | 弹层 `window.settings.{backgameData/browser/finishweb}`,与系统版本无关 |
|
||||||
| §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 文件重写互为补充 |
|
| §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/<gamedir>/<gamestart>/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/<gamedir>/<gamestart>/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/<gamedir>/<gamestart>/app_data.js
|
||||||
|
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_battery.js
|
||||||
|
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_network.js
|
||||||
|
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_gamesname.js
|
||||||
|
```
|
||||||
|
|
||||||
|
文件应包含 `var app_xxx=...;` 字面字符串。若文件不存在或为空 → AppDataWriter 没跑到 / 路径错;若文件正确但 H5 读 undefined → H5 端 `<script src>` 路径不对或加载时序问题。
|
||||||
|
|
||||||
|
**3. H5 console 对账 — Safari Web Inspector**
|
||||||
|
|
||||||
|
模拟器跑时 macOS Safari → 开发菜单 → Simulator → 当前 H5 页面 → Web Inspector → Console,逐项 console.log:
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log("app_version =", app_version); // 期望 1
|
||||||
|
console.log("app_channel =", app_channel); // 期望 ChannelConfig.plist channel 值
|
||||||
|
console.log("app_Launchtype =", app_Launchtype); // 大厅期望 0,⚠️ L 大写
|
||||||
|
console.log("app_getbattery =", app_getbattery); // ⚠️ 带 get 前缀
|
||||||
|
console.log("app_getnetwork =", app_getnetwork);
|
||||||
|
console.log("app_gamesname =", app_gamesname); // 期望 Array
|
||||||
|
```
|
||||||
|
|
||||||
|
任一项为 `undefined` → 拼写错误(大小写 / 前缀)或文件没加载到。常见错误:写成 `app_launchtype`(小写 l)/ `app_battery`(少 get)/ `app_getNetwork`(驼峰错)。
|
||||||
|
|
||||||
|
**4. 真机调试**:iPhone 上 Safari → 设置 → 高级 → 网页检查器开启,然后 macOS Safari 同样路径。
|
||||||
|
|
||||||
|
> ⚠️ 上面 H5 控制台和文件路径调试**仅在 Debug 配置生效**。Release 版本 os.log `.debug` 级别会被系统过滤掉,避免性能开销和敏感字段(agent / channel)外泄。Phase 9 Release 监控走 Sentry breadcrumb(§11.3),不依赖 console。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 能力模块详细设计
|
## 8. 能力模块详细设计
|
||||||
|
|||||||
Reference in New Issue
Block a user