收口桌面壳重放诊断日志

桌面壳 HostBridge replay 内部异常只记录固定阶段标签

扩展桌面壳配置门禁禁止 replay 日志输出 Rust 错误细节

补充宿主壳方案和共享决策中的 replay 诊断边界
This commit is contained in:
2026-06-20 23:55:44 +08:00
parent bb5fb5292c
commit a5e9e87107
4 changed files with 29 additions and 17 deletions
+12 -2
View File
@@ -2691,10 +2691,11 @@ const requiredRustHostSnippets = [
'HostBridgeReplayReservation',
'HOST_BRIDGE_RESPONSE_CACHE_MAX',
'Result<HostBridgeReplayReservation, HostBridgeResponse>',
'log_desktop_replay_failure("cache.lock", &error.to_string())',
'desktop host bridge replay failed for {label}: {error}',
'log_desktop_replay_failure("cache.lock")',
'desktop host bridge replay failed for {label}',
'replay_unavailable_response(request_id)',
'host_bridge_replay_state_returns_stable_error_when_cache_lock_is_unavailable',
'desktop_host_bridge_replay_logs_stable_label_only',
'.manage(HostBridgeReplayState::default())',
'fn prepare_host_bridge_request(',
'prepare_host_bridge_request(&mut request)',
@@ -2727,6 +2728,15 @@ if (
throw new Error('desktop WebView download diagnostics must not log URLs');
}
if (
desktopHostBridgeProtocolSource.includes(
'desktop host bridge replay failed for {label}: {error}',
) ||
desktopHostBridgeProtocolSource.includes('&error.to_string()')
) {
throw new Error('desktop HostBridge replay diagnostics must not log Rust error details');
}
assertSameList(
allowedTauriCommands,
['host_bridge_request'],
@@ -112,8 +112,8 @@ impl HostBridgeReplayState {
&self,
request_id: &str,
) -> Result<HostBridgeReplayReservation, HostBridgeResponse> {
let mut cache = self.cache.lock().map_err(|error| {
log_desktop_replay_failure("cache.lock", &error.to_string());
let mut cache = self.cache.lock().map_err(|_error| {
log_desktop_replay_failure("cache.lock");
replay_unavailable_response(request_id)
})?;
if let Some(slot) = cache.slots.get(request_id) {
@@ -140,8 +140,8 @@ impl HostBridgeReplayState {
) -> HostBridgeResponse {
let mut stored_response = match slot.response.lock() {
Ok(response) => response,
Err(error) => {
log_desktop_replay_failure("slot.complete", &error.to_string());
Err(_error) => {
log_desktop_replay_failure("slot.complete");
return replay_unavailable_response(&response.id);
}
};
@@ -153,16 +153,16 @@ impl HostBridgeReplayState {
pub(crate) fn wait_for_response(slot: Arc<HostBridgeReplaySlot>) -> HostBridgeResponse {
let mut stored_response = match slot.response.lock() {
Ok(response) => response,
Err(error) => {
log_desktop_replay_failure("slot.wait", &error.to_string());
Err(_error) => {
log_desktop_replay_failure("slot.wait");
return replay_unavailable_response(&slot.request_id);
}
};
while stored_response.is_none() {
stored_response = match slot.ready.wait(stored_response) {
Ok(response) => response,
Err(error) => {
log_desktop_replay_failure("slot.ready", &error.to_string());
Err(_error) => {
log_desktop_replay_failure("slot.ready");
return replay_unavailable_response(&slot.request_id);
}
};
@@ -185,11 +185,8 @@ impl HostBridgeReplaySlot {
}
}
fn log_desktop_replay_failure(label: &str, error: &str) -> bool {
if !error.is_empty() {
eprintln!("desktop host bridge replay failed for {label}: {error}");
}
fn log_desktop_replay_failure(label: &str) -> bool {
eprintln!("desktop host bridge replay failed for {label}");
false
}
@@ -466,6 +463,11 @@ mod tests {
assert_eq!(error.message, DESKTOP_HOST_BRIDGE_REQUEST_FAILED);
}
#[test]
fn desktop_host_bridge_replay_logs_stable_label_only() {
assert!(!log_desktop_replay_failure("slot.ready"));
}
#[test]
fn invalid_string_payload_is_rejected() {
let mut invalid = request("clipboard.writeText");
@@ -178,7 +178,7 @@
- 2026-06-18 HostBridge request id replay:Expo 和 Tauri 壳都必须按 request id 回放首次完成结果;同 id 进行中的请求共享同一执行结果,已完成请求直接回放缓存响应,避免系统分享、外链、剪贴板、文件选择 / 保存、本地通知、窗口导航等宿主副作用被重复触发。两端配置检查和测试会锁住 replay 结构。
- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod` 与 `normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。Expo 壳捕获原生异常时只透传共享 `HostBridgeError.code` 白名单内且 `message` 为字符串的协议错误,Tauri 壳的 `failed(...)` 出口也必须先校验同一错误码白名单;未知原生错误对象或非法错误码统一归一为 `host_error` 和固定失败文案,不把 native 私有字段、任意错误码或非字符串 message 回传给 H5。
- 2026-06-20 桌面 HostBridge command facade 单测边界:Tauri 唯一 `host_bridge_request` command 必须先通过 `prepare_host_bridge_request(...)` 做 envelope、method 和 request id 校验,再进入 `HostBridgeReplayState` reserve / wait / execute;`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs` 的单测必须覆盖非法 envelope 在 replay 前返回 `invalid_request` 且不会占用对应 request id 的 replay slot,桌面配置检查会反查该测试存在。
- 2026-06-20 桌面 HostBridge replay 内部失败边界:Tauri `HostBridgeReplayState` 的 cache lock、slot lock 和 condvar wait 异常不得 panic,也不得把 Rust 内部错误细节回传给 H5;桌面壳只写 `desktop host bridge replay failed for ...` stderr 观测日志,并统一返回 `host_error: desktop host bridge request failed`。桌面配置检查反查 `reserve(...)` 的 `Result` 出口、稳定错误响应和 poison lock 单测。
- 2026-06-20 桌面 HostBridge replay 内部失败边界:Tauri `HostBridgeReplayState` 的 cache lock、slot lock 和 condvar wait 异常不得 panic,也不得把 Rust 内部错误细节回传给 H5;桌面壳只写 `desktop host bridge replay failed for ...` 固定阶段标签,不把 mutex / condvar 错误文本写入 stderr,并统一返回 `host_error: desktop host bridge request failed`。桌面配置检查反查 `reserve(...)` 的 `Result` 出口、稳定错误响应、label-only 诊断和 poison lock 单测。
- 2026-06-20 移动 HostBridge runtime 能力回包边界:Expo `host.getRuntime` 回包里的 `capabilities` 必须直接等于共享契约 `HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES` 或 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES`,并使用与 `platform` 字段一致的归一平台值选择 profile;移动壳 runtime 单测和配置检查反查精确 profile 断言,避免 H5 实际消费的能力回包与入口 URL 能力 query 或共享 profile 分叉。
- 2026-06-20 移动 production bundle 宿主上下文边界:`apps/mobile-shell/scripts/check-expo-export.mjs` 必须读取 iOS / Android Metro export bundle,确认可分发 bundle metadata 是 Metro version 0、只包含当前平台 `fileMetadata`、指向 Hermes `AppEntry-*.hbc`,且 bundle 包含共享生产 H5 URL、`native_app`、`expo_mobile`、`hostCapabilities`、`hostVersion` 和 `bridgeVersion`,并不包含本机开发 H5 URL;移动壳配置检查反查 export smoke 的这些 token 和 metadata 结构门禁,避免 production bundle 丢失宿主上下文、混入本机入口或导出形态漂移。
- 2026-06-20 桌面 release 主窗口宿主上下文边界:Tauri release 配置只保留基础 `index.html`,Rust app 装配层必须在主窗口启动配置单测里断言补齐 `clientRuntime`、`clientType`、`hostShell`、`hostPlatform`、`hostVersion`、`bridgeVersion` 和 `hostCapabilities`;桌面单端配置检查反查这些断言存在,避免首屏 H5 丢失桌面壳运行态 query 后只靠 runtime 回读补救。
@@ -497,7 +497,7 @@ GameBridge 禁止:
2026-06-18 追加:HostBridge request envelope 校验收紧。共享契约提供 `isHostBridgeMethod` 和 `normalizeHostBridgeRequestId`;Expo 壳直接复用,Tauri 壳镜像同一 method 白名单和 request id 规则。空 id、控制字符 id、超长 id 和未知 method 都在进入 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。Tauri 唯一 `host_bridge_request` command 入口必须先经过 `prepare_host_bridge_request(...)`,再进入 `HostBridgeReplayState` reserve / wait / execute;非法 envelope 不得占用 replay slot,也不得触发任何宿主能力分发。
2026-06-20 追加:Tauri 桌面壳 HostBridge replay 的内部锁、等待和缓存状态异常只写入桌面壳 stderr 观测日志,H5 侧统一收到 `host_error` 与固定文案 `desktop host bridge request failed`;`HostBridgeReplayState::reserve(...)` 返回 `Result`,`complete(...)` / `wait_for_response(...)` 也不得用 `expect` 让宿主进程 panic。桌面配置检查会反查 replay 失败日志、稳定错误响应和 poison lock 单测,避免后续把内部 mutex / condvar 细节或 Rust panic 泄露到 H5 调用链。
2026-06-20 追加:Tauri 桌面壳 HostBridge replay 的内部锁、等待和缓存状态异常只写入桌面壳 stderr 观测日志,H5 侧统一收到 `host_error` 与固定文案 `desktop host bridge request failed`;stderr 只输出 `desktop host bridge replay failed for <stage>` 固定阶段标签,不记录 mutex / condvar 的 Rust 错误文本。`HostBridgeReplayState::reserve(...)` 返回 `Result`,`complete(...)` / `wait_for_response(...)` 也不得用 `expect` 让宿主进程 panic。桌面配置检查会反查 replay 失败日志、稳定错误响应、label-only 诊断和 poison lock 单测,避免后续把内部 mutex / condvar 细节或 Rust panic 泄露到 H5 调用链。
2026-06-20 追加:Expo 移动壳 `host.getRuntime` 回包里的 `capabilities` 必须直接等于共享契约中的 `HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES` 或 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES`,且使用同一个已归一的平台值选择 profile;移动壳 runtime 单测和配置检查会反查精确 profile 断言,避免 H5 实际消费的能力回包与入口 URL 或共享 profile 分叉。