为何 Tmds.DBus 查询在控制台正常,却在 AvaloniaUI 应用中抛出异常?
结论
在 CallMethodAsync 的读取回调中完成响应解析,只返回 Rgb 等独立数据。不要返回原始 Message,再在回调外调用 GetBodyReader()。
问题出在这段代码:
var reply = await connection.CallMethodAsync(
message,
(result, _) => result);
var reader = reply.GetBodyReader();
这段代码将库管理的响应消息带出了回调。如果所用的 Tmds.DBus.Protocol 实现会回收消息或复用缓冲区,回调结束后再读取消息,就可能访问到失效的数据。这与当前异常出现的位置吻合。
用 ReadInt64() 跳过返回值头部的做法也需要修正。ReadOne 返回的是 variant,应先读取其中的类型签名,再解析颜色结构体。
题目没有提供包版本,仅凭堆栈还无法确认该版本内部如何回收消息。排查时先修复这两处用法,再检查版本和运行环境。
为什么控制台正常,AvaloniaUI 却抛出异常?
await 不会自动复制响应消息,也不会延长消息缓冲区的有效期。
控制台应用通常没有 UI 同步上下文,await 后的代码可能很快就接着执行,恰好在消息被回收前完成读取。因此,即使控制台返回了正确颜色,也不能证明这种写法可靠。
AvaloniaUI 应用中的 await 通常会捕获 UI 同步上下文。响应到达后,后续代码可能需要排队,等待回到 UI 线程执行。等到调用:
reply.GetBodyReader();
底层消息数据可能已经被回收或重置。构造 Reader 所需的缓冲区范围此时已经无效,就可能抛出题目中的 ArgumentOutOfRangeException。
具体调度顺序取决于库版本和运行时,读取消息不应依赖这个顺序。添加 Task.Run、延时或 ConfigureAwait(false),都无法保证消息仍然有效。
修复:在回调中读取颜色
示例沿用问题中的 DBusConnection API,假定所用版本提供 Reader.ReadSignature(string)。如果 API 不同,需要按对应版本调整签名读取代码。
using Tmds.DBus.Protocol;
public async Task<Rgb?> GetThemeHexAsync()
{
try
{
var address = DBusAddress.Session;
if (string.IsNullOrEmpty(address))
{
return null;
}
using var connection = new DBusConnection(address);
await connection.ConnectAsync();
var writer = connection.GetMessageWriter();
writer.WriteMethodCallHeader(
destination: "org.freedesktop.portal.Desktop",
path: "/org/freedesktop/portal/desktop",
@interface: "org.freedesktop.portal.Settings",
member: "ReadOne",
signature: "ss");
writer.WriteString("org.freedesktop.appearance");
writer.WriteString("accent-color");
return await connection.CallMethodAsync(
writer.CreateMessage(),
(reply, _) =>
{
var reader = reply.GetBodyReader();
// ReadOne 返回 variant,其内部预期为 (ddd)。
reader.ReadSignature("(ddd)");
reader.AlignStruct();
var red = ToChannel(reader.ReadDouble());
var green = ToChannel(reader.ReadDouble());
var blue = ToChannel(reader.ReadDouble());
return new Rgb(red, green, blue);
});
}
catch (Exception e)
{
logger.Error(e, "Failed to read the desktop accent color.");
return null;
}
}
private static int ToChannel(double value)
{
if (double.IsNaN(value) ||
double.IsInfinity(value) ||
value < 0.0 ||
value > 1.0)
{
throw new InvalidOperationException(
"The accent color component is outside [0, 1].");
}
return (int)(value * 255.0);
}
解析在消息有效期间完成,返回的 Rgb 不再依赖响应缓冲区,连接也会在调用结束后释放。如果应用需要频繁查询或订阅设置变化,可以复用连接,由持有连接的服务统一管理其生命周期。
为什么要删除 ReadInt64()?
请求中的:
signature: "ss"
描述的是两个输入字符串,与响应类型不同。
ReadOne 的响应是一个 variant。对于有效的 accent-color 值,variant 内部应为 (ddd),也就是包含三个 double 的结构体。
原代码:
reader.AlignStruct();
reader.ReadInt64();
没有按这个类型读取数据。在 (ddd) 的编码中,variant 的签名和后面的对齐填充可能恰好占八个字节,所以 ReadInt64() 看起来像是“跳过了头部”,但响应中实际没有这个整数。
应先读取签名,再按结构体规则对齐:
reader.ReadSignature("(ddd)");
reader.AlignStruct();
当前异常发生在 GetBodyReader(),还没有执行到 ReadInt64(),因此这两处问题需要分别修复。
按顺序排查剩余问题
1. 先验证消息生命周期
只移动解析位置,保持目标服务、方法和参数不变。
如果移到回调内解析后异常消失,就有较强的依据判断问题出在消息生命周期。如果在回调内调用 GetBodyReader() 仍然失败,就需要继续检查所用版本的实现,不能再将原因归结为 UI 线程恢复较慢。
回调应保持同步。不要在其中保存 Message 或 Reader,也不要安排稍后执行的解析任务。
2. 比较实际加载的依赖版本
分别在控制台项目和 AvaloniaUI 项目中执行:
dotnet list package --include-transitive
比较 Tmds.DBus.Protocol 的实际版本,再用以下表达式确认运行时加载的程序集:
typeof(DBusConnection).Assembly.FullName
项目文件中的直接引用不足以确定实际加载的版本。是否存在依赖冲突,也需要根据证据判断。
3. 从相同桌面会话验证 portal
在应用所在的桌面会话中执行:
busctl --user call \
org.freedesktop.portal.Desktop \
/org/freedesktop/portal/desktop \
org.freedesktop.portal.Settings \
ReadOne ss \
org.freedesktop.appearance \
accent-color
检查方法是否可用、返回值是否符合预期。如果终端查询成功而应用失败,再比较两者的启动环境,尤其是 DBUS_SESSION_BUS_ADDRESS。从终端、IDE、桌面启动器或沙箱启动时,环境可能不同。
portal 后端也可能不支持该设置,或没有可用的强调色。应用应保留默认主题作为回退,避免将所有读取失败都视为启动失败。
4. 保留完整异常
使用日志框架提供的异常重载,例如 Serilog:
logger.Error(e, "Failed to read the desktop accent color.");
这样便于查看异常类型、调用栈和内部异常。如果收到 D-Bus 错误响应,还应记录错误名称和消息,以区分方法不可用、设置不存在和本地解析失败。
Avalonia 初始化代码还需要处理什么?
读取失败时,GetThemeHexAsync() 会返回 null,调用方必须先检查结果。主题和资源更新也应在 UI 线程执行:
using Avalonia.Media;
using Avalonia.Threading;
private async Task LoadMaterialThemeAsync(IServiceProvider provider)
{
using var scope = provider.CreateScope();
var themeLoader =
scope.ServiceProvider.GetRequiredService<ILinuxThemeLoader>();
var rgb = await themeLoader.GetThemeHexAsync();
if (rgb is not { } value)
{
return; // 保留已有主题。
}
await Dispatcher.UIThread.InvokeAsync(() =>
{
var color = new Color(
255,
(byte)value.Red,
(byte)value.Green,
(byte)value.Blue);
var materialTheme =
this.LocateMaterialTheme<CustomMaterialTheme>();
materialTheme.BaseTheme = BaseThemeMode.Inherit;
materialTheme.PrimaryColor = color;
materialTheme.SecondaryColor = color;
this.Resources["AccentColor"] = color;
});
}
OnFrameworkInitializationCompleted() 的返回类型是 void。将它改为 async void 后,框架无法等待其中的异步初始化完成。更稳妥的做法是先同步完成主窗口和基础主题初始化,再启动一个自行捕获异常的异步任务,读取强调色。
这是启动流程中的另一个问题。题目中的 LoadConfigurationAsync(...).ConfigureAwait(false) 位于颜色查询之后,无法解释此前 GetBodyReader() 抛出的异常。它影响的是该次等待之后代码的执行上下文。
备注:内容仅供参考。
