前言:此前的修复为什么“有效”,却又不够完整
此前曾记录过一次 KDE Plasma Wayland 环境下的 Fcitx5 冷启动故障。
当时的现象是:系统冷启动后,部分 Chromium 系应用无法正常切换输入法;重新启动 Fcitx5 后,问题又会消失。最终排查发现,系统同时存在两条 Fcitx5 启动路径:
- KWin Wayland 的 Virtual Keyboard / Input Method 机制;
- 传统的 XDG Autostart 机制。
两条路径在登录时同时尝试启动 Fcitx5,导致 D-Bus 名称竞争和初始化竞态。解决办法是在用户配置目录建立同名覆盖文件,将传统 XDG Autostart 项隐藏:
[Desktop Entry]
Type=Application
Name=Fcitx 5
Hidden=true
示例路径经过脱敏:
/home/demo/.config/autostart/org.fcitx.Fcitx5.desktop
这样便只保留 KWin Wayland 的官方输入法启动路径。
这项修复非常重要,而且确实解决了一个真实问题:
- Fcitx5 主实例保持唯一;
- 传统 Autostart 不再生成 systemd 用户单元;
Unable to request dbus name一类冲突消失;- 输入法在各类应用中恢复正常。
当时一度认为问题已经完整解决。
后来出现的新现象证明:Hidden=true 解决的是“双启动”,但没有解决所有与启动时序相关的问题。
新的问题不再是输入法不能使用,而是:
- 输入法实际切换正常;
- 中文、日文和英文输入均可用;
- 候选框正常;
- 托盘图标存在;
- 但在部分应用中,托盘图标不会随着输入法状态变化而更新。
这两个问题彼此相关,但并不是同一个问题。
一、环境与故障现象
测试环境经过脱敏后大致如下:
- Arch Linux;
- KDE Plasma 6;
- Wayland 会话;
- KWin 管理 Wayland 输入法;
- Fcitx5;
- Rime、Mozc 和英文键盘布局;
- 同时存在 XWayland,用于兼容部分旧应用。
最典型的现象是:
- 文件管理器中可以正常输入和切换输入法;
- Chromium 系浏览器中也可以正常输入;
- Markdown 编辑器中同样正常;
- 但系统托盘中的输入法图标,有时只在部分应用中同步,有时完全停留在旧状态。
这说明必须把两个概念分开:
输入法核心状态
负责:
- 按键处理;
- 输入法切换;
- 候选词窗口;
- 应用输入上下文;
- Wayland 输入法协议。
托盘显示状态
负责:
- 显示当前输入法图标;
- 输入法变化时更新图标;
- 向 KDE 系统托盘注册;
- 通过 StatusNotifierItem 或兼容机制传递状态。
因此,托盘状态错误并不意味着 Fcitx5 核心输入功能已经失效。
二、先确认双启动问题没有复发
由于此前已经发生过双启动,第一步仍然是确认旧问题没有回来。
检查结果显示:
- 当前只有一个
/usr/bin/fcitx5主进程; - KWin 的输入法配置仍然启用;
- 用户级
Hidden=true覆盖仍然存在; - systemd 用户生成目录中没有传统 Fcitx5 Autostart 单元;
- D-Bus 上没有第二个主实例竞争名称;
fcitx5-remote能正确查询和切换输入法;- Wayland 输入上下文正常存在。
这一步非常关键。
如果没有先排除双启动,很容易把后面的托盘问题错误归因于此前的老问题。实际上,新的故障发生在单一 Fcitx5 主实例正常运行的情况下。
换句话说:
第一篇文章中的
Hidden=true修复仍然正确,必须继续保留;只是它解决的范围没有最初想象得那么大。
三、托盘图标在 KDE 中有两条实现路径
进一步检查系统托盘后,发现 Fcitx5 的托盘图标可能通过两种方式进入 Plasma。
1. 原生 StatusNotifierItem
现代 KDE 应用通常直接注册 StatusNotifierItem,简称 SNI。
这种情况下:
- 托盘项目的 owner 是 Fcitx5 自己;
- 对象路径通常类似:
:1.xx/StatusNotifierItem
WindowId=0;- 图标和状态信号由 Fcitx5 直接发送给 KDE;
- 不需要经过旧式 X11 托盘代理。
这是理想路径。
2. XEmbed 兼容路径
旧式应用可能创建一个 X11/XEmbed 托盘窗口。
KDE 中的:
plasma-xembedsniproxy.service
会把这个旧式托盘窗口转换成 Plasma 能识别的 StatusNotifierItem。
这种情况下:
- 托盘项目由
xembedsniproxy代持; WindowId通常不是 0;- 图标更新需要经过:
Fcitx5
→ XEmbed 窗口
→ xembedsniproxy
→ StatusNotifierItem
→ Plasma 托盘
链路更长,也更容易产生代理同步问题。
四、第一次关键发现:重新建立 Fcitx5 运行态后,托盘路径发生变化
为了定位究竟是哪一层失效,进行了受控 A/B 测试。
依次测试:
- 重启
plasma-xembedsniproxy.service; - 重启
plasmashell; - 退出并重新激活 Fcitx5。
结果如下:
- 单独重启
xembedsniproxy没有恢复状态同步; - 单独重启
plasmashell也没有恢复; - 重新建立 Fcitx5 运行态后,图标立刻恢复正常。
更重要的是,恢复前后的托盘实现路径不同。
恢复前:
- Fcitx5 托盘由
xembedsniproxy代理; WindowId不为 0;- 输入正常,但托盘不同步。
恢复后:
- 托盘项目由 Fcitx5 自己持有;
WindowId=0;- 变成原生 StatusNotifierItem;
- 各应用中的托盘状态恢复同步。
这给出了一个非常重要的方向:
问题很可能不是简单的图标缓存,而是 Fcitx5 在启动时选择了不同的托盘注册路径。
五、一次错误尝试:禁用 xembedsniproxy
因为系统使用的是 Plasma Wayland,一度认为 XEmbed 属于旧式 X11 机制,或许可以直接禁用:
systemctl --user mask --now plasma-xembedsniproxy.service
这个判断是错误的。
下一次冷启动后:
- Fcitx5 核心输入功能仍然存在;
- 但托盘图标完全消失;
- 手动重新建立 Fcitx5 后,图标才恢复。
这说明 xembedsniproxy 虽然不是理想路径,却仍然是必要的兼容后备机制。
即使桌面会话是 Wayland,也不代表系统中不存在:
- XWayland;
- X11 兼容应用;
- 旧式托盘组件;
- 初始化阶段临时落入 XEmbed 的程序。
因此立即回退:
systemctl --user unmask plasma-xembedsniproxy.service
systemctl --user start plasma-xembedsniproxy.service
此后不再尝试禁用该服务。
这一段排查也澄清了两个完全不同的问题:
- 图标完全消失:是错误禁用
xembedsniproxy人为造成的; - 图标存在但状态不同步:才是原始问题。
前者回退 mask 后已经解决,不应继续与后者混在一起。
六、冷启动观察器与“观察者效应”
为了捕获完整冷启动时序,曾建立一个用户级 observer。
它收集:
- 用户 journal;
- 用户 D-Bus 消息;
- Fcitx5 状态;
- KWin、Plasma、kded6 和托盘相关进程;
- StatusNotifierItem 注册事件。
这次取证产生了一个非常庞大的 D-Bus 日志。
原始文本解压后约 1.9 GB,使用 Zstandard 压缩后只有约 3.8 MB。这种极高压缩率并不异常,因为全量 D-Bus 日志包含大量重复的:
- 接口名称;
- 对象路径;
- 字段结构;
- 状态信号;
- 相似消息模板。
然而,之后检查 observer 脚本时发现了一个严重问题:
脚本启动时调用了:
fcitx5-remote
fcitx5-remote -n
这些并不是纯粹的被动查询。
当 org.fcitx.Fcitx5 尚未存在时,访问该 D-Bus 名称会触发 Fcitx5 的 D-Bus 激活。
结果是:
- observer 在图形会话完全建立前启动;
- Fcitx5 被提前约二十多秒激活;
- 当时 KWin 尚未启动;
- Wayland 显示环境尚未完整可用;
- 日志出现
Failed to open wayland connection; - 主题生成器因为无法连接图形显示而崩溃。
因此,这次冷启动不能被视为“完全自然的冷启动”。
但这个被污染的实验仍然提供了重要线索:
当 Fcitx5、KWin launcher 和 StatusNotifierWatcher 的相对启动时间发生变化时,最终托盘路径也会发生变化。
在该次启动中,Fcitx5 虽然提前运行,但等 KWin 和 KDE 托盘服务出现后,最终成功注册了原生 SNI。
所以 observer 不是有效的长期方案,却帮助把调查方向收敛到启动顺序竞态。
冷启动记录也显示:Fcitx5 最终直接注册了原生 StatusNotifierItem,而不是先进入 XEmbed 后再切换。
七、静态审计:启动顺序究竟由谁决定
接下来不再继续盲目重启,而是对整个启动链进行静态审计。
1. KWin 的输入法入口
KWin 配置中存在类似:
[Wayland]
InputMethod[$e]=/usr/share/applications/fcitx5-wayland-launcher.desktop
VirtualKeyboardEnabled=true
这意味着 KWin 会根据指定的 Desktop 文件启动 Wayland 输入法 launcher。
2. 官方 launcher
Desktop 文件最终执行:
/usr/lib/fcitx5-wayland-launcher --reopen
该程序不是普通的自启动脚本,而是为 Wayland compositor 使用的专用 launcher。
它会读取 KWin 传入的:
WAYLAND_DISPLAY;WAYLAND_SOCKET;- Wayland socket 文件描述符;
- 用户会话环境。
--reopen 的作用,是让 Fcitx5 使用 KWin 提供的 Wayland 连接重新建立输入法前端。
3. Fcitx5 主进程的 D-Bus 激活
系统中还有:
/usr/share/dbus-1/services/org.fcitx.Fcitx5.service
其行为等价于:
[D-BUS Service]
Name=org.fcitx.Fcitx5
Exec=/usr/bin/fcitx5
因此,Fcitx5 主进程不一定由 launcher 直接 fork。
更准确的关系是:
- 某个组件访问
org.fcitx.Fcitx5; - 用户 D-Bus 激活
/usr/bin/fcitx5; - KWin 随后启动
fcitx5-wayland-launcher --reopen; - launcher 通知现有 Fcitx5 主进程建立正确的 Wayland 输入法连接。
4. StatusNotifierWatcher 的提供者
检查发现:
org.kde.StatusNotifierWatcher
并不是由 plasmashell 直接提供,而是由:
kded6
中的 StatusNotifierWatcher 模块注册。
因此,Watcher 何时可用,取决于:
- Plasma 用户 systemd target;
plasma-kded6.service;- kded6 模块初始化;
- D-Bus 名称注册完成时间。
5. 缺失的顺序依赖
systemd 的用户服务定义中,可以找到:
- KWin 与 Plasma target 的关系;
- plasmashell 的启动关系;
- xembedsniproxy 的启动关系;
- kded6 的启动位置。
但没有任何明确依赖保证:
org.kde.StatusNotifierWatcher 已准备好
↓
fcitx5-wayland-launcher 才运行
而 launcher 又是 KWin 直接启动的子进程,并不是一个方便添加 After= 的 systemd unit。
于是,以下事件实际上依赖每次登录时的运行速度:
- KWin 何时执行 launcher;
- kded6 何时注册 Watcher;
- Fcitx5 何时加载
notificationitem; - Fcitx5 何时建立 Wayland UI;
- 首次托盘注册时 Watcher 是否已经存在。
审计结果因此给出了高度可信的竞态模型:KWin launcher 与 kded6 提供的 StatusNotifierWatcher 之间没有显式顺序约束。
八、成功路径与故障路径
根据多轮现场数据,可以建立如下模型。
正常路径
KWin 启动
↓
StatusNotifierWatcher 已经准备好
↓
fcitx5-wayland-launcher 执行 --reopen
↓
Fcitx5 建立 Wayland classic UI
↓
notificationitem 注册原生 SNI
↓
Fcitx5 自己持有托盘项目
↓
WindowId=0
↓
托盘状态同步正常
故障路径
KWin 提前执行 launcher
↓
Fcitx5 开始建立 Wayland UI
↓
StatusNotifierWatcher 尚未准备好
↓
原生 SNI 首次注册未稳定完成
↓
进入 classicui/XEmbed 后备路径
↓
xembedsniproxy 代持托盘项目
↓
WindowId!=0
↓
实际输入正常,但图标更新可能失真
“Watcher 尚未就绪后,Fcitx5 是否缺少可靠重试”没有通过源码级证据完全确认,因此更严谨的表述应是:
原始故障高度可能发生在 Fcitx5 托盘注册时机与 KDE StatusNotifierWatcher 就绪时机之间。
但从工程角度看,已经足以设计一个最小、可逆的顺序修复。
九、为什么没有使用 systemd After=
最理想的方案本来是建立类似:
After=plasma-kded6.service
这样的正式依赖。
问题在于:
fcitx5-wayland-launcher不是独立的 systemd 用户服务;- 它是 KWin 直接启动的子进程;
- KWin 启动输入法 launcher 的行为来自
kwinrc中的 Desktop 文件; - 不能简单给 launcher 添加一个 systemd
After=。
也不适合把 Fcitx5 改成普通 systemd 自启动服务,因为这可能破坏:
- KWin 传入的
WAYLAND_SOCKET; - Wayland input-method 连接;
- compositor 与输入法之间的文件描述符传递。
因此选择了一个更小的方案:
保留 KWin 为唯一启动者,只在官方 launcher 前增加一个等待条件。
十、最终方案:等待 Watcher 后再执行官方 launcher
正式脚本放在用户自定义脚本目录中。
以下用户名、路径和脚本名均为脱敏示例:
/home/demo/bin/fcitx5-launcher-wait-tray
脚本的核心逻辑如下:
#!/bin/sh
set -eu
watcher='org.kde.StatusNotifierWatcher'
elapsed=0
interval_ms=100
timeout_ms=15000
while [ "$elapsed" -lt "$timeout_ms" ]; do
result=$(
busctl --user call \
org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus \
NameHasOwner \
s "$watcher" 2>/dev/null || true
)
case "$result" in
*"true"*)
printf '%s\n' "watcher-ready ${elapsed}ms" >&2
exec /usr/lib/fcitx5-wayland-launcher --reopen
;;
esac
sleep 0.1
elapsed=$((elapsed + interval_ms))
done
printf '%s\n' "watcher-timeout ${timeout_ms}ms" >&2
exec /usr/lib/fcitx5-wayland-launcher --reopen
这段脚本有几个重要设计原则。
1. 不直接启动 Fcitx5
脚本没有运行:
/usr/bin/fcitx5
因此不会新增第二条主进程启动入口。
2. 不调用 fcitx5-remote
这样不会通过 D-Bus 提前激活 Fcitx5,也不会重复 observer 的错误。
3. 不直接调用 Watcher 的功能接口
脚本只向 D-Bus 自身询问:
org.kde.StatusNotifierWatcher 是否已经有 owner
也就是 NameHasOwner。
这不会主动激活 Watcher,只会判断它是否已经真正注册成功。
4. 不使用固定延迟
没有简单地写:
sleep 1
因为不同启动中,Watcher 的准备时间可能不同。
当前逻辑是:
- Watcher 一旦就绪,立即继续;
- 系统快时不浪费时间;
- 系统慢时多等待一会;
- 最长只等待有限时间。
5. 超时后仍继续启动
即使 Watcher 因其他故障一直没有出现,15 秒后仍然执行官方 launcher。
这样可以避免托盘服务异常时,输入法本身也被永久阻塞。
6. 使用 exec
最终执行:
exec /usr/lib/fcitx5-wayland-launcher --reopen
exec 会用官方 launcher 替换当前 wrapper 进程。
这样可以保留 KWin 传入的:
- 环境变量;
- Wayland socket;
- 已打开文件描述符;
- D-Bus 会话地址。
不会额外产生一个后台父进程,也不会绕开 KWin 的官方输入法连接机制。
十一、用户级 Desktop 文件
新的用户级 Desktop 文件放在标准位置:
/home/demo/.local/share/applications/fcitx5-launcher-wait-tray.desktop
内容以系统官方文件为模板,只修改名称和 Exec:
[Desktop Entry]
Name=Fcitx 5 Wayland Launcher with Tray Wait
Comment=Wait for KDE StatusNotifierWatcher before launching Fcitx5 Wayland bridge
Exec=/home/demo/bin/fcitx5-launcher-wait-tray
Terminal=false
Type=Application
NoDisplay=true
实际实施时应保留官方 Desktop 文件中的其他兼容字段,不应凭空删减。
随后把 KWin 配置从:
InputMethod[$e]=/usr/share/applications/fcitx5-wayland-launcher.desktop
改为:
InputMethod[$e]=/home/demo/.local/share/applications/fcitx5-launcher-wait-tray.desktop
VirtualKeyboardEnabled=true 保持不变。
修改前先对配置文件进行备份。
示例备份:
/home/demo/.config/kwinrc.20260101-120000
备份时间戳来自原文件时间,而不是备份执行时的当前时间;文件名不使用 .bak。
十二、第一次冷启动验证
第一次重启后,日志显示:
watcher-ready 900ms
也就是说,wrapper 实际等待了约 900 毫秒,随后立即 exec 官方 launcher。
机器侧检查确认:
- KWin 确实调用了用户 wrapper;
- wrapper 已被官方 launcher 替换;
- 没有残留第二个包装进程;
- Wayland 环境变量仍然存在;
- KWin 传入的 socket 文件描述符仍然保留;
waylandim正常加载;- 当前只有一个 Fcitx5 主实例;
- 没有传统 XDG Autostart 单元;
- 没有 D-Bus 名称竞争;
- Fcitx5 托盘项由 Fcitx5 自己持有;
WindowId=0;- 没有由 xembedsniproxy 代理的 Fcitx5 项。
人工检查也全部通过:
- 文件管理器:输入正常,托盘同步;
- Chromium 系浏览器:输入正常,托盘同步;
- Markdown 编辑器:输入正常,托盘同步。
第一次启动的机器侧和人工侧均通过。
当时发现脚本日志中存在一个纯格式错误:
printf %sn ...
导致日志显示:
watcher-ready 900msn
该错误不影响等待逻辑和 exec 行为,但为了避免后续审计歧义,将其修正为:
printf '%s\n' ...
修正前保留了时间戳备份。
十三、第二次冷启动验证
修正日志格式后,再进行一次完全独立的冷启动。
第二次仍然显示:
watcher-ready 900ms
再次确认:
- KWin 输入法配置仍指向用户 Desktop 文件;
- wrapper 确实被调用;
- 官方 launcher 正常完成短生命周期任务后退出;
- 只有一个 Fcitx5 主实例;
waylandim正常;- 没有传统 Autostart;
- 没有
Unable to request dbus name; - 没有
Failed to open wayland connection; - 没有本轮新增的相关崩溃;
- StatusNotifierWatcher 由 kded6 提供;
- Fcitx5 自己注册原生 StatusNotifierItem;
WindowId=0;- xembedsniproxy 正常运行,但没有代理 Fcitx5 项目。
三个测试应用再次全部通过。
至此,方案连续两次独立冷启动验证成功。
两次都等待 900 毫秒并不异常。脚本以固定间隔轮询,而 KDE 组件启动时间在同一系统上通常较稳定,所以两次很可能恰好落在同一轮检测上。
十四、完成后的清理
排查过程中创建了不少临时内容,包括:
- 冷启动 observer;
- 用户 systemd unit;
- 全量 D-Bus monitor;
- pkexec 重启脚本;
- 环境检查脚本;
- 临时用户总线日志;
- 临时 systemd 启用链接。
最终遵循一个原则:
只保留具有实际功能的正式文件和备份;调查结束后删除无功能遗留。
工作站上最终保留:
/home/demo/bin/fcitx5-launcher-wait-tray
/home/demo/.local/share/applications/fcitx5-launcher-wait-tray.desktop
/home/demo/.config/kwinrc
/home/demo/.config/autostart/org.fcitx.Fcitx5.desktop
以及脚本和 kwinrc 的时间戳备份。
删除:
- 冷启动 observer unit;
- observer 启用残留;
- 全量 monitor 进程;
- 临时 pkexec 文件;
- 一次性环境检查脚本。
完整取证目录被转移到另一台服务机的审计输出目录中保存。
其中约 1.9 GB 的原始 D-Bus 日志压缩后只有约 3.8 MB,因此完整保留并不会造成明显空间负担。
十五、最终结论:这是两层修复,而不是一次修复
整个问题最终包含两个相互独立、但彼此关联的层次。
第一层:解决双启动
使用用户级同名 XDG Autostart 覆盖:
Hidden=true
目的:
- 禁止传统 Autostart 启动 Fcitx5;
- 只保留 KWin Wayland Input Method 路径;
- 消除第二实例和 D-Bus 名称竞争。
这一项修复一直是正确的,也必须继续保留。
第二层:解决托盘注册时序竞态
通过用户 wrapper:
等待 org.kde.StatusNotifierWatcher 有 owner
↓
exec 官方 fcitx5-wayland-launcher --reopen
目的:
- 保持 KWin 为唯一 launcher 调用者;
- 保留 KWin 传递的 Wayland socket;
- 确保关键重连动作发生时 KDE 托盘服务已准备好;
- 稳定走 Fcitx5 原生 StatusNotifierItem;
- 避免落入可能状态不同步的 XEmbed 代理路径。
因此,此前那篇文章并没有错。
更准确的评价是:
Hidden=true完整解决了 Fcitx5 双启动问题,但没有完整解决 Fcitx5 在 Plasma Wayland 登录过程中的所有初始化竞态。
最初看到输入恢复正常后,很容易把“双启动”和“托盘初始化”视为同一问题。后来出现“输入正常、图标存在、状态却不同步”,才证明托盘注册还有独立的时序问题。
现在两部分分别得到处理:
Hidden=true
→ 保证只有一个 Fcitx5 主实例
等待 StatusNotifierWatcher
→ 保证官方 Wayland launcher 在正确时机运行
两者结合后,连续两次冷启动均表现正常:
- 单一 Fcitx5 主实例;
- Wayland 输入上下文正常;
- 原生 SNI;
WindowId=0;- 多个应用输入正常;
- 托盘图标切换同步。
十六、这次排查带来的经验
1. “输入正常”不代表“输入法整体正常”
输入处理、候选框和托盘图标可能属于不同链路。
2. 同一个程序可能同时使用现代和兼容机制
Fcitx5 可以直接注册原生 SNI,也可能落入 XEmbed 后备路径。
3. Wayland 会话不等于可以禁用所有 X11 兼容组件
XWayland 和旧式托盘应用仍然可能依赖 xembedsniproxy。
4. 重启后恢复通常意味着初始化状态被重建
它不是永久修复,但可以用来寻找分叉点。
5. 监控脚本也可能改变被监控对象
调用 D-Bus 客户端命令可能触发服务激活。所谓“只读查询”不一定真的是旁路观察。
6. 固定 sleep 通常不是理想的竞态修复
等待明确条件比等待固定秒数可靠。
7. 最小修复应尽量保留官方启动链
没有直接运行第二个 Fcitx5,而是继续让 KWin 调用官方 launcher,只补上缺失的就绪条件。
8. 验证至少要包含两次独立冷启动
一次成功只能证明“可以成功”;连续独立重启更能说明顺序控制确实稳定。
9. 排查结束后必须清理临时产物
临时 unit、监控脚本和一次性重启文件不应长期留在系统中。
结语
这次故障表面上只是“输入法图标不跟着切换”,实际涉及:
- KWin Wayland Input Method;
- Fcitx5 D-Bus 激活;
- Wayland socket 传递;
- kded6;
- StatusNotifierWatcher;
- 原生 StatusNotifierItem;
- XEmbed;
- xembedsniproxy;
- systemd 用户会话;
- XDG Autostart;
- 启动时序竞态。
最终没有采用粗暴的“登录后重启 Fcitx5”,也没有通过禁用兼容组件掩盖问题,而是在保留 KDE 官方输入法启动链的前提下,只补充了一个缺失的条件:
系统托盘服务真正就绪之后,才让 KWin 的官方 Fcitx5 Wayland launcher继续执行。
这是一种非常小的改动,却解决了一个长期、间歇、难以复现的桌面初始化问题。