From c780d6aeff4cc71f19de45a5ac7f0a2e81387322 Mon Sep 17 00:00:00 2001 From: dailz Date: Mon, 22 Jun 2026 18:13:42 +0800 Subject: [PATCH] =?UTF-8?q?docs(test=5Fportal):=20=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=20Portal=20=E5=86=92=E7=83=9F=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/test_portal.rs | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/examples/test_portal.rs b/examples/test_portal.rs index 4630a5f..d2b1708 100644 --- a/examples/test_portal.rs +++ b/examples/test_portal.rs @@ -1,11 +1,41 @@ +//! XDG Portal 权限冒烟测试示例。 +//! +//! 本示例演示完整的 Portal ScreenCast 授权流程,分四步: +//! 1) 创建 Screencast proxy → 2) 创建 session → 3) 选择源(显示器/窗口) +//! → 4) start() 触发系统授权对话框(用户点击"共享"后返回流信息)。 +//! +//! 运行:`cargo run --example test_portal`(参见 AGENTS.md "Useful manual commands")。 +//! +//! Rust 异步模型(与 Go 对比): +//! - Go 用 goroutine + channel;async 函数本身**惰性**,需 runtime 驱动。 +//! - 本示例刻意**不用** `#[tokio::main]` 宏,而是手动 `Runtime::new()` + `block_on` +//! (与 src/backend_detect.rs 同款"手动 runtime"模式);这是因为 ashpd 内部缓存 +//! zbus::Connection 到全局 OnceLock,若宏自动建的 runtime 被 drop, +//! 缓存的 connection 会"僵尸化"导致后续 hang(详见 AGENTS.md)。 +//! +//! 对照 Go:`go func() { ... }()` ≈ `tokio::spawn(async { ... })`; +//! 而 `block_on` 类似 Go 的 `select {}` 阻塞 main goroutine 等待退出。 + +// ashpd = XDG Portal 的 Rust 高层绑定,封装了 D-Bus ScreenCast 接口 use ashpd::desktop::screencast::{CursorMode, Screencast, SelectSourcesOptions, SourceType}; +// PersistMode 控制"恢复令牌"持久化级别(DoNot / Persistent / ExplicitlyRevoked) use ashpd::desktop::PersistMode; +// BitFlags = 位域集合类型(一个值可同时包含多个 SourceType,类比 Go 的 iota | 操作) use ashpd::enumflags2::BitFlags; +// 同步 main → 手动创建 tokio Runtime → block_on 阻塞驱动 async 块。 +// 这种"同步外壳 + 异步内核"的写法等价于 `#[tokio::main] async fn main()`, +// 但保留了显式控制 runtime 生命周期的灵活性(参见文件头说明)。 fn main() { + // 手动创建 tokio runtime(含 reactor + executor + 时间驱动); + // unwrap() 仅示例用;生产代码应返回 Result 并 `?` 传播(但 fn main 不返回 Result) let rt = tokio::runtime::Runtime::new().unwrap(); + // block_on 阻塞当前线程直到传入的 future 完成;这是同步↔异步边界 rt.block_on(async { + // async {} 块构造一个匿名 future,仅在 block_on poll 时才执行(惰性,与 goroutine 不同) eprintln!("1. Creating Screencast proxy..."); + // Screencast::new() 内部通过 D-Bus 连接 org.freedesktop.portal.ScreenCast; + // .await 让出执行权直到 future 就绪(Go 没有这个语法,需 channel/锁模拟) let proxy = match Screencast::new().await { Ok(p) => { eprintln!(" OK"); @@ -13,11 +43,14 @@ fn main() { } Err(e) => { eprintln!(" FAIL: {e}"); + // early-return 仅退出 async 块(不是退出 main),block_on 返回 () return; } }; eprintln!("2. Creating session..."); + // create_session 建立一个 ScreenCast 会话句柄; + // Default::default() 用类型默认参数(ashpd 推断为 SessionOptions,所有字段取 Default) let session = match proxy.create_session(Default::default()).await { Ok(s) => { eprintln!(" OK"); @@ -30,7 +63,15 @@ fn main() { }; eprintln!("3. Selecting sources..."); + // BitFlags 表达"可选多显示器/窗口/工作区"集合; + // 这里 `into()` 将单个 Monitor 转为位域(Go 类似 flag = 1 << iota) let sources: BitFlags = SourceType::Monitor.into(); + // Builder 链式:每次 set_X 返回 &mut Self(类似 Go functional-options 模式但更显式) + // - cursor_mode Embedded:光标嵌入帧内 + // - sources: 仅 Monitor(去掉窗口,简化授权 UX) + // - multiple=false:单选(一次只授权一个显示器) + // - persist_mode DoNot:不申请恢复令牌(避免持久权限残留) + // 整个 builder 链构造一个 future,末尾的 .await 等待 D-Bus 返回 let result = proxy .select_sources( &session, @@ -50,6 +91,8 @@ fn main() { } eprintln!("4. Starting (should show dialog)..."); + // start() 触发系统授权对话框(D-Bus 调用阻塞直到用户响应); + // 第二参数 parent_window = None(无父窗口,常见于 CLI 程序) let response = match proxy.start(&session, None, Default::default()).await { Ok(r) => { eprintln!(" OK"); @@ -60,6 +103,8 @@ fn main() { return; } }; + // Portal D-Bus 响应是双层结构:外层是 Request::response(Ok/Err), + // 内层才是 ScreenCast 流信息(streams() 返回 PipeWire 节点 + dmabuf 信息列表) match response.response() { Ok(r) => eprintln!(" Got {} stream(s)", r.streams().len()), Err(e) => eprintln!(" Response error: {e}"),