大上 Paperlike HD 使用有一段时间了,然而有一点我对其非常不满:它需要以 root 权限运行一个图形界面的程序。具体麻烦的地方是:
- 图形界面的程序不方便使用 systemd 管理,那个窗口我得找个地方安放,并且在登出图形界面或者 Xorg 出问题时会随之关闭
- 即使持续运行此程序,当几秒内不使用键盘或者鼠标的时候屏幕就会休眠。这导致我无法将此屏幕用于关注程序日志或者聊天工具的新消息。
- 它持续不断地执行多个线程的任务(读取键盘事件、读取鼠标事件、通过 ioctl 与设备通讯),耗费了不少 CPU
- 在屏幕尚未连接时,它的运行会导致内核不断输出日志「drm_dp_i2c_do_msg: 2 callbacks suppressed」
我曾多次想自己实现一个符合自己使用习惯的方案。
首先当然是 strace 上去啦。这会得到许多类似这样的消息:
ioctl(9</dev/i2c-1<char 89:1>>, _IOC(_IOC_NONE, 0x7, 0x7, 0), 0x7f47d8805b70) = 1 nanosleep({tv_sec=0, tv_nsec=100000000}, NULL) = 0 ioctl(9</dev/i2c-1<char 89:1>>, _IOC(_IOC_NONE, 0x7, 0x7, 0), 0x7f47d8805be0) = 1 nanosleep({tv_sec=0, tv_nsec=200000000}, <unfinished ...>
可以看到它在对 /dev/i2c-1 这个文件进行操作,但是具体内容是个指针,strace 看不到。
我尝试过使用大名鼎鼎的 IDA 的免费版本来分析其具体行为。但我对 IDA 并不熟悉,并且 IDA 只支持 Intel 语法的汇编,而我见的 AT&T 语法的比较多,Intel 的很多表示法我不太能看懂。
后来根据 ioctl 的请求参数找到这个文档,里边有这些 i2c 消息的结构体定义。于是想着先把 ioctl 的数据弄出来看看。一开始尝试用 gdb 去看那个地址的数据,但想到数据是变动的,再加上 gdb 查看太累了,就想起了通过 LD_PRELOAD 去 hook ioctl。
所以又要写 C 了?并没有呢!C 写起来那么不舒服,还是用 Rust 吧~然后搜了一下,还真有现成的用于写 LD_PRELOAD 库的 crate,比如我用的 redhook。不用自己去 dlopen,不用在各处写很多错误处理代码,很容易就写好了。代码链接。
拿到了 ioctl 里用的消息,我不用理会它具体是什么意思,也没办法去猜测,自然是把它按大上提供的程序那个样子给发过去了。于是又一个 Rust 程序出来了。
一开始写的时候不小心往 unsafe 代码块里传了个悬空的指针,导致程序不工作,调试了好久,甚至我都把完整的整个流程给复刻了一遍。这要是用 C 写文本解析的逻辑可头疼了,不过 Rust 写起来就跟 Python 差不多的了~
至于那个 bug,是 Rust 语句中的临时对象(此例中是包含一个对象的数组)会在语句结束之后就释放导致的。有点坑,但也没什么好的办法。
程序运行起来之后就会保持 Paperlike HD 显示器可用,不会报错让装驱动,也不会过几秒就休眠了。我大幅降低了消息发送的频率(由差不多每秒三次改成了三秒才一次),再加上不需要读取键鼠输入,所以 CPU 占用也会大幅减少。另外内核也不会再打印「drm_dp_i2c_do_msg: 2 callbacks suppressed」的消息了,大概是因为消息频率降低了?重新连接显示器之后,和大上原版程序一样有概率出现显示器亮蓝灯、屏幕不工作的情况。拔插一下电源可解。
当然啦,如果有人要用这个程序的话,记得先确认一下你的 i2c 设备文件路径(去 lsof 大上原版程序就行)。另外,使用此程序后果自负,由此造成的任何设备损坏或者其它损失,我都不负责任的哦~