最近在折腾一个“云桌面”的东西——不是那种给你几个网页版管理面板的“云主机”,而是一个真的 Linux 桌面:窗口、任务栏、文件管理器、输入法、声音,能装软件、能扫码登录 QQ,用浏览器或者一个客户端连上去就能用。这算我们(七亿网络)自己折腾的一块,跟平时做的远程管理不是一回事。

做到“能用”只花了几天,真正卡人的地方全在细节里。挑几个印象最深的记一下,都是“现象很吓人、根因很朴素”的那种。

一、剪贴板:四个独立的 bug,叠成一个“功能不通”

剪贴板这功能,我前后修了四轮,每次都“感觉好了”,第二天还是不能用。最后发现是四个互不相干的 bug 排队等着我:

  1. API 是异步的,我按同步写了。
    某版本 Electron 里 clipboard.readText() 返回的是 Promise<string>,不是老版的字符串。我写 t = clipboard.readText(),拿到的是个 Promise 对象——它 truthy、没有 length、每次都是新对象,于是“内容变了”的判断永远成立,日志疯狂刷屏,真正的文本反而传不过去。
    教训:别猜一个 API 是同步还是异步,去看 electron.d.ts。 同一个模块里 safeStorage.encryptString() 又是同步的,一起改了必翻车。
  2. 事件监听表漏了一个键,被静默丢掉。
    事件系统里 listeners = { connected: [], disconnected: [] }——漏了 clipboard。于是 on('clipboard', fn) 走进 if (listeners[name]) 为假的判断,被安静地丢弃;反向 emit 时又对 undefined 做 for...of 抛错。没有报错,没有日志,就是“监听好像没注册上”。
  3. 方法名和私有字段名对不上。
    上行用了一个“发送剪贴板”的方法,那个方法在当前版本里根本不叫这个名字;还有一个守卫判断读了 rfb._rfb_connection_state,而实际字段是驼峰无下划线的 _rfbConnectionState——取到 undefined,条件恒假,调用被跳过。
  4. 脚本加载顺序。
    下行怎么都不通、日志一条没有——renderer 根本没收到事件。根因是 index.html 里那个 <script type="module"> 排在普通 <script> 前面。模块脚本永远晚于普通脚本执行,而我在普通脚本里写了“如果插件对象存在就注册监听”——那一刻它还不存在,整块被跳过。
    教训:跨 module / classic 边界的“加载即注册”必踩,改成“就绪轮询 + 一次绑定”。

四个 bug 分别属于 API 假设、数据结构、字段命名、执行时机四个层面。它们唯一的共同点是:都表现得像“剪贴板坏了”。 排障最怕的就是这种——你以为是同一个老问题,其实每次都是新问题。

二、输入法:进程起得太早

桌面里的中文输入法死活打不出中文,但直接敲字母是好的。

用诊断工具一看:输入法进程在跑,但“输入上下文数量 = 0”——也就是说,没有任何应用连到它。

根因很朴素:启动脚本里,输入法进程是在会话总线建立之前拉起来的。而所有应用是通过会话总线去找输入法的,总线还没建就先把输入法拉起来,等于它落在一个没人知道的角落。后来把顺序改成“先建总线,再在同一环境里起输入法”,立刻就好了。

验证方法值得记下来:别靠“我打了字看有没有中文”这种主观判断,用工具看 IC 数;最终实锤是让终端 cat > 文件 ,切中文打「nihao」,然后 xxd 看字节是不是 e4bda0 e5a5bd(“你好”)。

输入法诊断:输入上下文数量 IC=0,异常偏高亮(示意图,非真实截图)

示意图(非真实截图)

三、视频流延迟:花了一晚上,发现瓶颈在缓冲

桌面画面走的是“整屏当视频传”的方案(代价换好处:任意桌面、任意软件都能用,不用逐个适配)。但操作延迟一直偏高,同一个网络下用户拿它和 Windows 远程桌面比,觉得“没那个跟手”。

我先按直觉去调编码:降分辨率、换参数、开 60 帧……结果收益都很小,还引入了不稳。后来翻客户端日志才对上:日志里直接有“当前缓冲落后实时多少秒”这一项,它一直在 0.2~0.5 秒之间跳。再一测链路延迟,只有 21 毫秒——网络根本不是瓶颈。

延时的真身就是那 0.2~0.5 秒的播放缓冲。 把目标缓冲从 0.2 秒压到 0.12 秒,抖动时再自动回退,体验立刻好一截。

顺手还发现一个反直觉的坑:视频元素长时间处于 display:none 时,浏览器不肯正常往前缓冲,“等它就绪再显示”的逻辑直接死锁——画面就是迟迟不出现。改成“就绪或有画面尺寸就显示”。

至于为什么同类产品更跟手:它们传的是“绘制指令 / 区域”,本地重绘;我们传的是“整屏视频”。同一网络下前者输入延迟天然更低,这是路线差异,不是参数没调好。要追平得换协议,工程量大得多。

客户端日志:缓冲落后实时 0.2~0.5s,链路 RTT 仅 21ms(示意图,非真实截图)

示意图(非真实截图)

四、两个“UI 谎言”

  • 断线遮罩永远盖着。 用户一直报“连上就被遮罩盖住 / 连接已断开”,查了一整夜隧道、服务器、协议层,全好的。真相是 CSS:一个 .overlay{display:flex} 把 HTML 的 hidden 属性吃掉了——遮罩从启动那一刻就永远显示。画面底层一直是通的,是 UI 在撒谎。修法就一行:[hidden]{display:none!important}。
    教训:UI 布局 bug 先看截图,再查网络层。
  • 嵌入的网页只渲染出顶部一条,下面全黑。 根因是给内嵌视图元素加了 display:block——该版本下这会把它内部的视口高度卡死在默认的 150px。宿主元素尺寸是满的,内部却一直只有 150 高。改回默认的 display:flex 就好。
    教训:内嵌视图别乱加 display:block。

这种“表面正常、底下没跑”的假象我们以前也踩过——写过一篇《systemctl 说 active,可 ps 里连 mysqld 的影子都没有》,是同一个道理:别信“状态”,要看实据。

五、装个软件而已,为什么这么难

这是最“非技术”的一个坑:用户从官网下了个 .deb,双击没反应。

现实里的桌面发行版各不相同:有的缺中文字体包、有的语言文件被镜像配置排除掉、官网上给的下载链接是 404 的……最后给桌面加了一个“双击 deb 直接装”的处理器(带确认和进度提示),才算让“装软件”这件事配得上“云桌面”这个名字。

这类活儿没有聪明解法,就是一个环境一个环境地趟。 远程这块我们之前也写过一篇《零安装做 Windows 群控:一套键鼠广播到几十台被控》,思路一样——把麻烦消化在工具里,别甩给用户。

小结

云桌面这个方向,技术栈不复杂,难在把一整套原本要硬件 + 系统 + 驱动配合的东西,全塞进一个远程会话里,还要让人觉得“就是台电脑”。

几个通用心得:

  • 现象相同 ≠ 根因相同:一个“功能不通”背后可能是四个独立 bug。
  • 别信 API 的直觉:同步 / 异步、私有字段名、加载顺序,都要查实。
  • 先拿数据再下手:延时那晚要是先去翻日志里的缓冲值,能省一整晚。
  • UI 会在撒谎:截图比推理快。

东西还在打磨,先把这段记下来。