收口桌面壳文件诊断日志
桌面壳文件导入导出失败只记录固定阶段标签 扩展桌面壳配置门禁禁止文件日志输出路径和读写错误细节 补充宿主壳方案和共享决策中的文件诊断边界
This commit is contained in:
@@ -1199,8 +1199,8 @@ function assertDesktopDialogBoundary(method, functionName, filterLabel, expected
|
||||
}
|
||||
const failureLogger =
|
||||
action === 'save'
|
||||
? 'log_desktop_file_export_failure("path.convert", &error.to_string())'
|
||||
: 'log_desktop_file_import_failure("path.convert", &error.to_string())';
|
||||
? 'log_desktop_file_export_failure("path.convert")'
|
||||
: 'log_desktop_file_import_failure("path.convert")';
|
||||
if (!fileFunctionBody.includes(failureLogger)) {
|
||||
throw new Error(`desktop shell ${method} must log native path conversion errors`);
|
||||
}
|
||||
@@ -2587,20 +2587,21 @@ const requiredRustHostSnippets = [
|
||||
'file_import_unavailable_response',
|
||||
'log_desktop_file_export_failure',
|
||||
'log_desktop_file_import_failure',
|
||||
'desktop file export failed for {label}: {error}',
|
||||
'desktop file import failed for {label}: {error}',
|
||||
'log_desktop_file_export_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_import_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.text", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.image", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.audio", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.text.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.document.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.image.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.audio.join", &error.to_string())',
|
||||
'desktop file export failed for {label}',
|
||||
'desktop file import failed for {label}',
|
||||
'log_desktop_file_export_failure("path.convert")',
|
||||
'log_desktop_file_import_failure("path.convert")',
|
||||
'log_desktop_file_export_failure("write.text")',
|
||||
'log_desktop_file_export_failure("write.image")',
|
||||
'log_desktop_file_export_failure("write.audio")',
|
||||
'log_desktop_file_import_failure("read.text.join")',
|
||||
'log_desktop_file_import_failure("read.document.join")',
|
||||
'log_desktop_file_import_failure("read.image.join")',
|
||||
'log_desktop_file_import_failure("read.audio.join")',
|
||||
'desktop_file_cancelled_responses_are_stable',
|
||||
'desktop_file_unavailable_responses_are_stable',
|
||||
'desktop_file_failures_are_logged_without_exposing_native_detail',
|
||||
'desktop_file_failures_log_stable_label_only',
|
||||
'"file export cancelled"',
|
||||
'"file import cancelled"',
|
||||
'"file export unavailable"',
|
||||
@@ -2744,6 +2745,24 @@ const requiredRustHostSnippets = [
|
||||
'replay_state.complete(slot, response)',
|
||||
];
|
||||
|
||||
for (const blockedDesktopFileLogSnippet of [
|
||||
'desktop file export failed for {label}: {error}',
|
||||
'desktop file import failed for {label}: {error}',
|
||||
'log_desktop_file_export_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_import_failure("path.convert", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.text", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.image", &error.to_string())',
|
||||
'log_desktop_file_export_failure("write.audio", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.text.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.document.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.image.join", &error.to_string())',
|
||||
'log_desktop_file_import_failure("read.audio.join", &error.to_string())',
|
||||
]) {
|
||||
if (desktopHostBridgeFilesSource.includes(blockedDesktopFileLogSnippet)) {
|
||||
throw new Error('desktop shell file module must hide native file errors');
|
||||
}
|
||||
}
|
||||
|
||||
const desktopNavigationStateScriptBody = extractFunctionBody(
|
||||
desktopShellNavigationSource,
|
||||
'desktop_navigation_state_script',
|
||||
|
||||
@@ -31,19 +31,13 @@ fn file_import_unavailable_response(request: &HostBridgeRequest) -> HostBridgeRe
|
||||
)
|
||||
}
|
||||
|
||||
fn log_desktop_file_export_failure(label: &str, error: &str) -> bool {
|
||||
if !error.is_empty() {
|
||||
eprintln!("desktop file export failed for {label}: {error}");
|
||||
}
|
||||
|
||||
fn log_desktop_file_export_failure(label: &str) -> bool {
|
||||
eprintln!("desktop file export failed for {label}");
|
||||
false
|
||||
}
|
||||
|
||||
fn log_desktop_file_import_failure(label: &str, error: &str) -> bool {
|
||||
if !error.is_empty() {
|
||||
eprintln!("desktop file import failed for {label}: {error}");
|
||||
}
|
||||
|
||||
fn log_desktop_file_import_failure(label: &str) -> bool {
|
||||
eprintln!("desktop file import failed for {label}");
|
||||
false
|
||||
}
|
||||
|
||||
@@ -66,8 +60,8 @@ pub(crate) async fn export_desktop_host_bridge_text_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("path.convert", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_export_failure("path.convert");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -75,12 +69,12 @@ pub(crate) async fn export_desktop_host_bridge_text_file(
|
||||
tauri::async_runtime::spawn_blocking(move || write_export_text_file(path, content)).await;
|
||||
let bytes = match export_result {
|
||||
Ok(Ok(bytes)) => bytes,
|
||||
Ok(Err(error)) => {
|
||||
log_desktop_file_export_failure("write.text", &error.to_string());
|
||||
Ok(Err(_error)) => {
|
||||
log_desktop_file_export_failure("write.text");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("write.text.join", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_export_failure("write.text.join");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -109,8 +103,8 @@ pub(crate) async fn import_desktop_host_bridge_text_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("path.convert");
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -119,8 +113,8 @@ pub(crate) async fn import_desktop_host_bridge_text_file(
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.text.join", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.text.join");
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
@@ -143,8 +137,8 @@ pub(crate) async fn import_desktop_host_bridge_document_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("path.convert");
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -153,8 +147,8 @@ pub(crate) async fn import_desktop_host_bridge_document_file(
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.document.join", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.document.join");
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
@@ -179,8 +173,8 @@ pub(crate) async fn export_desktop_host_bridge_image_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("path.convert", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_export_failure("path.convert");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -188,12 +182,12 @@ pub(crate) async fn export_desktop_host_bridge_image_file(
|
||||
tauri::async_runtime::spawn_blocking(move || write_export_bytes_file(path, bytes)).await;
|
||||
let byte_count = match export_result {
|
||||
Ok(Ok(byte_count)) => byte_count,
|
||||
Ok(Err(error)) => {
|
||||
log_desktop_file_export_failure("write.image", &error.to_string());
|
||||
Ok(Err(_error)) => {
|
||||
log_desktop_file_export_failure("write.image");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("write.image.join", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_export_failure("write.image.join");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -221,8 +215,8 @@ pub(crate) async fn import_desktop_host_bridge_image_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("path.convert");
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -233,8 +227,8 @@ pub(crate) async fn import_desktop_host_bridge_image_file(
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.image.join", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.image.join");
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
@@ -254,8 +248,8 @@ pub(crate) async fn import_desktop_host_bridge_audio_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("path.convert", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("path.convert");
|
||||
return file_import_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -264,8 +258,8 @@ pub(crate) async fn import_desktop_host_bridge_audio_file(
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id.clone(), payload),
|
||||
Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error),
|
||||
Err(error) => {
|
||||
log_desktop_file_import_failure("read.audio.join", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_import_failure("read.audio.join");
|
||||
file_import_unavailable_response(request)
|
||||
}
|
||||
}
|
||||
@@ -290,8 +284,8 @@ pub(crate) async fn export_desktop_host_bridge_audio_file(
|
||||
};
|
||||
let path = match file_path.into_path() {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("path.convert", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_export_failure("path.convert");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -299,12 +293,12 @@ pub(crate) async fn export_desktop_host_bridge_audio_file(
|
||||
tauri::async_runtime::spawn_blocking(move || write_export_bytes_file(path, bytes)).await;
|
||||
let byte_count = match export_result {
|
||||
Ok(Ok(byte_count)) => byte_count,
|
||||
Ok(Err(error)) => {
|
||||
log_desktop_file_export_failure("write.audio", &error.to_string());
|
||||
Ok(Err(_error)) => {
|
||||
log_desktop_file_export_failure("write.audio");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
Err(error) => {
|
||||
log_desktop_file_export_failure("write.audio.join", &error.to_string());
|
||||
Err(_error) => {
|
||||
log_desktop_file_export_failure("write.audio.join");
|
||||
return file_export_unavailable_response(request);
|
||||
}
|
||||
};
|
||||
@@ -359,21 +353,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn desktop_file_failures_are_logged_without_exposing_native_detail() {
|
||||
assert!(!log_desktop_file_export_failure(
|
||||
"path.convert",
|
||||
"private export detail"
|
||||
));
|
||||
assert!(!log_desktop_file_export_failure(
|
||||
"write.text",
|
||||
"private export detail"
|
||||
));
|
||||
assert!(!log_desktop_file_import_failure(
|
||||
"path.convert",
|
||||
"private import detail"
|
||||
));
|
||||
assert!(!log_desktop_file_import_failure(
|
||||
"read.text.join",
|
||||
"private import detail"
|
||||
));
|
||||
assert!(!log_desktop_file_export_failure("path.convert"));
|
||||
assert!(!log_desktop_file_export_failure("write.text"));
|
||||
assert!(!log_desktop_file_import_failure("path.convert"));
|
||||
assert!(!log_desktop_file_import_failure("read.text.join"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_file_failures_log_stable_label_only() {
|
||||
assert!(!log_desktop_file_export_failure("write.image.join"));
|
||||
assert!(!log_desktop_file_import_failure("read.audio.join"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3157,6 +3157,7 @@
|
||||
|
||||
- 背景:Tauri 桌面壳已声明文本 / 文档 / 图片 / 音频导入导出;这些能力会打开系统文件对话框、转换系统路径并在后台线程读写文件。如果系统路径转换、后台读写或任务 join 异常只被折叠成稳定 HostBridge 错误,H5 语义安全,但开发侧无法区分系统对话框路径异常、文件系统失败或后台任务失败。
|
||||
- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/files.rs` 必须在导出路径转换、导出写入、导入路径转换和导入后台读取 join 失败时记录 `desktop file export failed for ...` 或 `desktop file import failed for ...` 日志。HostBridge 对 H5 仍只返回稳定错误语义,不透传本地路径、系统错误或线程细节;用户取消系统文件对话框仍返回 `cancelled`,不记录为异常。
|
||||
- 2026-06-21 调整:桌面文件导入导出失败日志只记录 `desktop file export failed for <stage>` / `desktop file import failed for <stage>` 固定标签,不输出本地路径转换错误、文件读写错误、后台任务 join 错误或其它系统细节;配置检查拒绝 `files.rs` 重新拼接 `: {error}` 或把路径 / 读写 / join 错误传给日志函数。
|
||||
- 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::files`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-06-20 桌面壳窗口状态小能力系统异常必须可观测
|
||||
|
||||
@@ -352,7 +352,7 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-20 追加:桌面壳剪贴板和本地通知插件异常必须可观测。Tauri clipboard-manager 读写失败、notification permission state / request 失败、系统通知 show 失败时,桌面壳必须分别记录 `desktop clipboard failed for ...` 或 `desktop notification failed for ...` 日志;HostBridge 回包仍只暴露稳定 `clipboard write unavailable`、`clipboard read unavailable`、`notification permission unavailable` 或 `notification delivery unavailable` 语义。该约束不新增遥测 SDK、后台通知、自动更新或 H5 直连 Tauri JS 插件。2026-06-21 调整:桌面剪贴板和本地通知失败日志只记录 `desktop clipboard failed for <stage>` / `desktop notification failed for <stage>` 固定标签,不把 clipboard-manager、notification 插件错误或系统细节写入 stderr。
|
||||
|
||||
2026-06-20 追加:桌面壳文件导入导出的系统异常必须可观测。Tauri dialog 路径转换失败、后台文件读写任务失败或任务 join 失败时,桌面壳必须分别记录 `desktop file export failed for ...` 或 `desktop file import failed for ...` 日志;HostBridge 回包仍只暴露稳定 `file export unavailable` 或 `file import unavailable` 语义,不透传本地路径、系统错误或线程细节。用户取消系统文件对话框仍返回 `cancelled`,不记录为异常。
|
||||
2026-06-20 追加:桌面壳文件导入导出的系统异常必须可观测。Tauri dialog 路径转换失败、后台文件读写任务失败或任务 join 失败时,桌面壳必须分别记录 `desktop file export failed for ...` 或 `desktop file import failed for ...` 日志;HostBridge 回包仍只暴露稳定 `file export unavailable` 或 `file import unavailable` 语义,不透传本地路径、系统错误或线程细节。用户取消系统文件对话框仍返回 `cancelled`,不记录为异常。2026-06-21 调整:该日志只记录 `desktop file export failed for <stage>` / `desktop file import failed for <stage>` 固定标签,不把本地路径转换、文件读写或后台任务 join 错误写入 stderr。
|
||||
|
||||
2026-06-20 追加:桌面壳窗口状态小能力的系统异常必须可观测。Tauri 主窗口缺失、主题读取失败、角标设置失败或窗口标题设置失败时,桌面壳必须分别记录 `desktop appearance failed for ...`、`desktop app badge failed for ...` 或 `desktop window title failed for ...` 日志;HostBridge 回包仍只暴露稳定 `appearance unavailable`、`badge unavailable` 或 `window title unavailable` 语义,不透传系统错误、窗口内部信息或平台细节。2026-06-21 调整:桌面外观、角标和窗口标题失败日志只记录 `desktop appearance failed for <stage>` / `desktop app badge failed for <stage>` / `desktop window title failed for <stage>` 固定标签,不把 Tauri `theme()` / `set_badge_count` / `set_title` 错误详情或主窗口缺失细节写入 stderr。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user