1
23
2024
1

如果你发现你的 OOM Killer 在乱杀进程

请对 systemd 做如下设置,避免它将你的用户进程调整为更容易被杀:

建立 /etc/systemd/user.conf.d/oom.conf 文件,并写入:

[Manager]
DefaultOOMScoreAdjust=0

建立 /etc/systemd/system/user@.service.d/resources.conf 文件(及其中间目录),并写入:

[Service]
OOMScoreAdjust=0

systemd 的默认设置也会干扰火狐浏览器自己的设定,造成与预期相反的行为。

 

Category: Linux | Tags: linux systemd
7
25
2023
2

btrfs 元数据满了怎么办

上一篇《btrfs 翻车记》记叙了我们服务器上的 btrfs 出事的情况,好像吓到一些用户了 QAQ。其实那次情况比较特殊啦。一般来说,就算元数据用满了,也不至于改内核代码才能救回来。不过元数据满的问题确实困扰了许多用户,正好这些天群里有不少人遇到了,本文就记录一下元数据满了之后如何处置。

问题和处置

问题的现象是部分文件操作报错「No space left on device」,但是 df 等工具明明报告还有空间。btrfs filesystem usage 的输出是这样:

btrfs filesystem usage 截图

我们可以看到,还有 373G 的空闲空间(Free)呢。但是呢,「Device unallocated」已经不足 1G 了。在充分大的文件系统上,btrfs 会以 1G 为单位来分配块组(block group,简称 bg)。所以现在这个情况,已经无法分配新的 bg 啦。然后我们再往下看,「Metadata」的部分,总共 4.5G,已经用了 4G。还剩下 512M,刚好是 Global reserve 的大小。也就是说,不算保留空间的话,元数据已经没有空间可用了,所以才会报错。

那现在怎么办呢?如果文件系统上有不需要的很大的文件,并且没有快照,删除后空间可以立即释放的话,可以删除试试,看看能不能刚好空出来一个 bg。不然就试着跑一下 balance,像这样:

btrfs balance -dusage=0 截图

这个命令是说,把使用率为 0% 的数据 bg 整理一下。从输出「had to relocate 0 out of ...」可以看出,没有这样的 bg,操作没有效果。可以试试增加 -dusage 的值,看看能不能成功。很遗憾,这个案例中未能成功:

btrfs balance -dusage=1 截图

那只有另外添加一些空间来腾挪数据了。如果有闲置的分区(或者暂时用不上的 swap 分区)就可以拿来用。不然的话插个U盘也行。不需要多大,几个 G 就行。挪好数据就可以去掉了,不会长期使用的。另一种很有风险的做法是使用内存来暂存数据,但这样一旦死机或者断电,整个文件系统就完蛋了,不建议使用。

准备好空闲分区后,就可以 btrfs device add 添加上去了。这里通常要加 -f 参数,抹除分区里原有的数据。注意不要添加错设备了哦。

btrfs device add 截图

可以看到,设备添加上去之后,又可以往文件系统里写数据了。接下来跑 btrfs balance -dusage=10 之类的命令腾些数据 bg 出来就好了。这里要注意只 balance 数据 bg,不要动元数据的 bg,因为元数据越是集中存放,将来就越可能需要分配新的 bg,就越有可能遇到没 bg 可以分配的情况。

btrfs-heatmap 工具可以查看 bg 的分布和使用情况。以下是 balance 好之后的状态(使用 --curve linear 参数):

btrfs heatmap

图中,白色的是数据 bg,蓝色的是元数据 bg。颜色越亮,使用率越高。纯黑的是未分配空间。可以看到,这里有大量用得不多的数据 bg。balance 操作就是把它们给合并了一些,空出来不少黑色区域(最下方的黑色部分是新添加的设备上的未分配空间)。这是 usage 截图:

btrfs filesystem usage 截图

算一算,除去新添加设备的空间,原存储设备上也能有 46G 的未分配空间了(看上去新添加设备并没开始使用,只是让 btrfs 相信它有足够的空间用)。接下来把之前添加的设备删除就可以了:btrfs device del /dev/sdb1 /。等它运行完毕就可以拔掉该设备了(如果是可移动介质的话)。

预防

这个问题的本质就是 bg 的碎片化导致明明看上去有空间,但是元数据用不了,因此报错,需要手动处理。要识别即将出问题的文件系统也很简单:btrfs filesystem usage 看一看,如果「unallocated」很小(不足 1G)就要赶紧 balance 一下了(当然前提是有不少碎片化的空闲空间)。注意,这个时候千万不要删快照!删快照可能会快速消耗保留的元数据空间,从而导致添加设备都加不上、还报错只读的情况。

btrfs 最近添加了自动块组回收(automatic block group reclaim)功能,但默认并没有启用。因为是新功能,可能会有 bug,你也不知道它会什么时候运行,所以我暂时不建议使用。自己写个定时脚本,在系统空闲的时候运行也不错的。


本文中的图像素材均由遇到问题的群友提供。

Category: Linux | Tags: linux btrfs
7
6
2023
3

btrfs 翻车记

如标题所言,我用了多年的 btrfs,终于还是遇到翻车啦。由于文件系统翻车了,相关日志内容缺失,所以本文我仅凭记忆描述事件,就不提供准确的日志输出了。

事件经过

出事的是 archlinuxcn 的编译机。那天中午时分我就收到了 Grafana 给我发送的莫名其妙的报警邮件,称某个监控项无数据了。我去面板上瞅了半天,明明有数据的啊。不解,但是忙别的事情去了,也没有细究。晚些时候,我又收到了好些同类告警,遂登录机器打算检查 Grafana 日志。但操作过程中,退出 zsh 的时候我好像看到了写命令历史时出现「read-only filesystem」的字样?于是检查了一下,发生大事不好了,文件系统真变只读了!

这个 btrfs 是我上次迁移机器的时候换上的,因为我觉得过了这么多年,btrfs 挺稳定了啊,而定时快照很方便,devtools 也支持通过快照来快速创建打包用的 rootfs(虽然大部分时候都在 tmpfs 上打包了用不上)。但我们编译机一直以来有个问题:硬盘有多少用多少。前一任编译机用的 1T 硬盘,是刚刚够用,现在换 2T 了,结果用着用着又只剩下200多GiB啦……肥猫最近开始玩 bees 去重了,听说在其它机器上效果显著。不过这台编译机快照多,这「蜂群」嗡嗡了几天都没完事,还出事了。

btrfs filesystem usage 一看就发现,空闲200多G数据是真的(因此没有触发相关报警),但是元数据满了,也没有未分配空间了。这种事群里有多位群友遇到过了,问题不大,加个设备再 balance 一下就好了。我一开始是这么想的,刚好有两个挺大的 swap 分区能够用来腾挪。结果 btrfs 告诉我,文件系统只读,添加设备失败!那好,我 remount rw 一下。结果 btrfs 说文件系统出毛病了,不支持 remount rw!

这个时候我才感觉事情有点难办了。这个 btrfs 文件系统是 /,并不能卸载啊。没有找到在线修理的办法,只好呼叫凤凰卷,通过 iDRAC 进入 archiso。期间服务器重启了一次,但连过去依旧是只读的。进到 archiso 之后,尝试抢在报错之前添加设备,但是并没有成功。会卡住一会儿,然后报错「No space left on device」。按 farseerfc 的建议,clear_cache 和 zero-log 都试过了,但并没有解决问题。有人建议把大文件 truncate 一下,看看能不能刚好释放出 1G 的连续空间出来,但是我有定时快照呀,truncate 了也不会立即释放空间。最后卷直接下单了新机器,开始 btrfs send……

事后

服务器迁移还比较顺利。数据接收完毕,网络配置更新一下,引导器装好,重启,熟悉的编译机就回来啦~除了 nvchecker 好像跑得有点慢?怎么 ping Google 要 60ms 的?原来是忘记更新 /etc/resolv.conf 了,里边还写着旧 ISP 的 DNS 服务器地址呢。systemd-resolved 这次做了回好事,把 DNS 服务 fallback 到了 9.9.9.9。DNS 解析慢是 fallback 过程造成的,而 ping Google 延迟高,是因为 9.9.9.9 不知道怎么回事,给解析到比较远的地方去了。

新编译机 CPU 比之前那台快了不少,硬盘也增加到了 3.4T。挺好的,除了这时机不太好,旧编译机还有近一个月到期……另外由于是突发状况,所以没有及时缩短 DNS TTL,导致迁移完成之后 DNS 解析没有及时跟上(隔天我陆陆续续从另一台使用这台编译机转发邮件的机器那里收到了好些邮件,都是抱怨这编译机连不上的)。

蜂群(bees)也重新开始工作了。这次快照较少,我还专门为了它们暂停过自动快照,过了一段时间之后首次扫描终于完成了。之后它们就能很快跟上进度,不会消耗大量 CPU 了。

我添加了定时任务来执行 btrfs balance start -dusage=10 /,每周释放一些使用率低的数据块组,避免空间分配了又不怎么用,到最后明明有剩余空间却让元数据无处可写。

farseerfc 对出事的 btrfs 进行了更多不同方案的修复尝试,但依然未能修好。

一些抱怨

没想到我用了这么多年的 btrfs,还是被坑到了。明明还有不少空闲空间,但是 btrfs 不知道用。我看到最近有个「automatic block group reclaim」特性,支持自动回收块组了,但是搜索结果第一项结果是今年年初有人在邮件列表上报告说它有问题……出现问题 ro 挺好的,但是这个状态下不让进行维护操作就太难受了。作为 / 文件系统使用时,对于远程机器来说,即使有 iDRAC 或者 IPMI 之类的东西,用起来也费事,还不得不中断可能还活着的服务。而对于不支持远程访问的机器就更麻烦了,比如在家办公时办公室的机器,或者出差旅游探亲时在家的机器。我也考虑过在 initramfs 里配网络、开 sshd,但是并没有现成的工具,事发时再配的话,一次性成功的可能性太低了。

至于事发原因,蜂群(bees)只是加快了元数据空间的使用(dedupe 快照的结果),其本身并没有问题。出事重启之后,在再次被挂载为只读之前,还是写入了不少数据,包括一次成功的快照(后来查 pacman 数据库损坏的问题时发现的)。这可能是后续添加设备都无法成功的原因。

以前用的 ext4,在文件系统快满时只是碎片化严重、效率降低,它甚至还会给 root 保留一部分空间来处理问题。后来用 zfs,快满了就 0B/s,等于废掉。现在 btrfs 遇到空间不足也没有好太多,变只读了。(我还打算抱怨一下新文件系统可靠性不如旧的来着,想想前不久在群里看到 btrfs 抓到了位反转,还是不抱怨了。大家各有千秋。)


2023年07月08日更新:farseerfc 把它救活了!核心方法是把这里的 global reserve 大小由 512M 改成 2G。因为之前重启了一次,那时不仅成功创建了一个新快照,还删掉了一个旧的。然后它删着删着就把 512M 的 global reserve 给用完了,就报错、事务回滚,于是就过不去了。和邮件列表上这个问题是一样的:Global reserve and ENOSPC while deleting snapshots on 5.0.9 — Linux BTRFS

2023年07月25日更新:其实本文所述内容是罕见情况啦,并没有多少人会遇到的,大家不用害怕。另外新写了一篇《btrfs 元数据满了怎么办》,记录大多数人遇到的元数据满的问题如何解决。

Category: Linux | Tags: linux btrfs
6
29
2023
2

在 nspawn 里运行 docker

有个服务器需要维护,因此需要将其上的所有服务暂时迁移走。打算直接扔到另一台比较闲的服务器上,直接拿 systemd-nspawn 跑起来得了。简单方便,除了网络之外不需要额外配置。但问题是,这些服务里包含一个使用 docker 运行的 ElasticSearch,在同为容器的 nspawn 里跑会有问题吗?

试了一下,还真有的问题。dockerd 会报权限错误而跑不起来。但稍微搜一下就找到了解决方案:

SYSTEMD_SECCOMP=0 systemd-nspawn --capability=all --network-bridge=br0 --boot -D rootfs

nspawn 默认会限制一些权限。这样可以让其不做任何限制,相当于 docker 的 --privileged 参数。然后就可以嵌套着跑啦。

跑起来之后检查一下,所有服务均正常运作了,没有任何问题。把网络配好,外边的 nginx 负责一下转发(因为懒所以没配外网 IP),就可以接替工作啦。

PS: 迁移的过程中遇到了一个小坑。rsync 不加 --numeric-ids 的话会尽量保持用户名不变,等里边的系统跑起来就各种权限问题了。所以转移 rootfs 的时候一定得记着加上 --numeric-ids

Category: Linux | Tags: linux systemd docker
2
9
2023
6

新的 PaddleOCR 部署方案

PaddleOCR 发布 2.6 版本了,支持 Python 3.10 啦,于是可以在 Arch Linux 上跑了~所以我决定再部署一次。

我之前跑 PaddleOCR 有两个方案,使用 chroot 加一大堆 systemd 的限制选项,以及使用 bwrap 和用户命名空间。

chroot 的方案总感觉不知道限制够了没。实际上当初那篇文章写完我就意识到这服务怎么用我的 uid 在跑啊,乱发信号好像还能把我的进程都杀掉的样子。另外这个 chroot 其实是我用来学习、研究和适配 Debian 用的,并不是专门跑这个服务的,感觉有点——怎么说呢——碍事?总之不太好。

bwrap 方案更干净一些,不过创建起来挺麻烦的(所以我才只部署了一次嘛)。不使用用户命名空间可能会简单一些,但那样就是用我的用户在跑了。

所以这次我决定试试方案,使用 systemd-nspawn。另外(再次)尝试了使用 NVIDIA GPU 的版本,把我电脑上闲得发慌的 GeForce 940MX 显卡给用上了。

过程

首先去 Arch 镜像里的 iso/latest/ 目录下载个 archlinux-bootstrap-x86_64.tar.gz 回来。在 /var/lib/machines 下创建个叫 paddleocr 的 btrfs 子卷 / zfs 文件系统 / 普通目录用来存放新的 rootfs。sudo bsdtar xf ...... -C /var/lib/machines/paddleocr 解压出来。记得一定要用 bsdtar 以避免丢失某些文件元信息(虽然我不知道那些信息有啥用但是有警告就是不爽嘛)。

然后就可以 systemd-nspawn -M paddleocr 拿到个 shell 了。这里边只安装了 base 和 arch-install-scripts。可以先修改 pacman 镜像然后 pacman -Syu python 滚一下顺便装上 Python。然后 useradd -s /bin/bash -m -U paddleocr 创建个跑 paddleocr 的用户。su - paddleocr 切过去,python -m venv venv 创建虚拟环境,然后进去按 PaddleOCR 的文档装就行了。装好运行起来没问题之后,写个 for 循环把所有支持的语种都识别一遍,以下载各语言的模型(当然你也可以只下载你想要的)。做好之后可以清一下缓存啥的。gdu 就挺好用的。

哦,以上是 CPU 版本的安装流程。GPU 版本的可没有这么简单。首先要把显卡设备传进这个 nspawn 里。创建 /etc/systemd/nspawn/paddleocr.nspawn 文件,然后里边写上:

[Exec]
ResolvConf=off
NoNewPrivileges=true
User=paddleocr

[Files]
Bind=/run/paddleocr
Bind=/var/cache/pacman/pkg

Bind=/dev/nvidia0
Bind=/dev/nvidiactl
Bind=/dev/nvidia-modeset
Bind=/dev/nvidia-uvm
Bind=/dev/nvidia-uvm-tools

[Network]
Private=true

哦,这里有挂载 pacman 缓存目录前边忘了说,不过这个不重要啦。这里指定了用户,但是可以在命令行上用 -u root 覆盖的,不影响进去维护。私有网络,也就是给它配置个网络命名空间,里边除了 lo 外啥网络接口都没有。那它怎么访问网络呢?它访问不了网络啦。所以要 bind mount 进去一个 /run/paddleocr,用于通信的 UNIX 域套接字将会放在这里。网络不通,走文件系统就好啦。

然后找台机器把 AUR 包 cuda-10.2 和 cudnn7-cuda10.2 打一下,但是不用安装。我们不搞 CUDA 开发,里边有一大堆东西都是不需要的。把需要的库复制进 rootfs 里去就行了。至于需要什么库?进那个虚拟环境的 Python 里,import paddle 然后 paddle.utils.run_check() 跑一下就知道了。复制库之后记得跑 ldconfig 啊。

PaddleOCR 能跑起来之后,就可以把我的服务丢进去跑啦。最终命令长这样:

sudo systemd-nspawn -M paddleocr --user=paddleocr /home/paddleocr/paddleocr-http --loglevel=warn -j 4

-j 参数是限制并发识别数的,避免过载 CPU 或者 GPU,并不是线程数。

跑起来之后,sudo setfacl -m u:$USER:rwx /run/paddleocr/http.sock 给自己授权,然后 curl 一下试试:

time curl -sS -F file=@a.png -F lang=zh-Hans --unix-socket /run/paddleocr/http.sock http://localhost:5174/api | jq .

对于小图片的话挺快的,不到一秒就能出结果。我使用 CPU 版本跑的话,会慢个近十倍的样子。顺便说一下,这是我对服务进行性能优化之后的结果。之前每张图都开新进程跑太慢了。大概是需要加载一大堆库,然后把模型上传到 GPU,每张图一进程的话 GPU 版本反而会明显慢于 CPU 版本。代价是服务会一直占用大约 2G 内存,即使你并没有在用。

系统挂起到内存或者休眠到磁盘时,内存里的内容是被保留了,但是 GPU 显存并没有,大概因此会报 cuda runtime error 999。这时候,只需要停止服务,卸载 nvidia_uvm 内核模块然后重新加载,再启动服务就可以恢复了。如果 nvidia_uvm 卸载不掉的话,那就没办法了,要么重启,要么改用 CPU 版本。NVIDIA 是有个把显存 dump 到内存里存起来的方案的,但是没必要啊,尤其是休眠到磁盘上的时候,多浪费时间啊。

文件下载

你可以直接用我做好的文件。通过本地的 IPFS 服务访问:

http://localhost:8080/ipns/k51qzi5uqu5di433o42zgqk2xck3y160q1hyvqbyyerd36au2pk0c2jw3hcqxx/

你也可以用别的网关来访问,都一样。如果 IPNS 解析失败的话,试试

http://localhost:8080/ipfs/QmNV31bApmgRcHCQjGufQ3zrFDaf6JBWvBt8pU2TA2Baz6/

我把用于跑服务、设置权限的配置文件打了个 Arch 软件包。nspawn 用的 rootfs 也打包上传了。PaddleOCR CPU 和 GPU 版本是分开的,所以有两个包。CPU 版本的 nspawn 叫 paddleocr-cpu,服务名也是。把 rootfs 解压到正确的地方之后,systemctl start paddleocr 或者 paddleocr-cpu 就好啦。用户需要加入 paddleocr 组才能访问 HTTP 套接字哦。

如果遇到CUDA error(803), system has unsupported display driver / cuda driver combination报错,请将系统当前的 libcuda.so.1 复制进 nspawn 里:

sudo cp /usr/lib/libcuda.so.1 /var/lib/machines/paddleocr/usr/local/lib

另外服务配置文件放到 GitHub 上了:paddleocr-service

Category: Linux | Tags: linux systemd OCR
9
7
2022
17

让离线软件真正离线

去年我做了个索引 Telegram 群组的软件——落絮,终于可以搜索到群里的中文消息了。然而后来发现,好多消息群友都是通过截图发送的,落絮就索引不到了。也不能不让人截图嘛,毕竟很多人描述能力有限,甚至让复制粘贴都能粘出错,截图就相对客观真实可靠多了。

所以落絮想要 OCR。我知道百度有 OCR 服务,但是我显然不会在落絮上使用。我平常使用的 OCR 工具是 tesseract,不少开源软件也用的它。它对英文的识别能力还可以,尤其是可自定义字符集所以识别 IP 地址的效果非常好,但是对中文的识别能力不怎么样,图片稍有不清晰(比如被 Telegram JPEG 压缩)、变形(比如拍照),它就乱得一塌糊涂,就不说它给汉字之间加空格是啥奇怪行为了。

后来听群友说 PaddleOCR 的中文识别效果非常好。我实际测试了一下,确实相当不错,而且完全离线工作还开源。但是,开源是开源了,我又没能力审查它所有的代码,用户量太小也不能指望「有足够多的眼睛」。作为基于机器学习的软件,它也继承了该领域十分复杂难解的构建过程,甚至依赖了个叫「opencv-contrib-python」的自带了 ffmpeg、Qt5、OpenSSL、XCB 各种库的、不知道干什么的组件,试图编译某个旧版 numpy 结果由于太旧不支持 Python 3.10 而失败。所以我决定在 Debian chroot 里安装,那边有 Python 3.9 可以直接使用预编译包。所以问题来了:这么一大堆来源不明的二进制库,用起来真的安全吗?

我不知道。但是我知道,如果它联不上网的话,那还是相对安全的。毕竟我最关心的就是隐私安全——一定不能把群友发的图片泄漏给未知的第三方。而且联不上网的话,不管你是要 DDoS 别人、还是想挖矿,收不到指令、传不出数据,都行不通了嘛。我只要它能从外界读取图片,然后把识别的结果返回给我就好了。

于是一个简单的办法是,拿 bwrap 给它个只能访问自己的独立网络空间它不就访问不了互联网了吗?不过说起来简单,做起来还真不容易。首先,debootstrap 需要使用 root 执行,执行完之后再 chown。为了进一步限制权限,我使用了 subuid,但这也使得事情复杂了起来——我自己都难以访问到它了。几经摸索,我找到了让我进入这个 chroot 环境的方法:

#!/bin/bash -e

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --cap-add ALL \
  --uid 0 \
  --gid 0 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --share-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  -- \
  /bin/bash -l

这里给了联网权限,是因为我需要安装 PaddleOCR。没有在创建好 chroot 之后、chown 之前安装,是因为我觉得拿着虽然在 chroot 里但依旧真实的 root 权限装不信任的软件实在是风险太大了。装好之后,再随便找个图,每种语言都识别一遍,让它下载好各种语言的模型,接下来它就再也上不了网啦(为避免恶意代码储存数据在有网的时候再发送):

#!/bin/bash -e

dir="$(dirname $2)"
file="$(basename $2)"

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --uid 1000 \
  --gid 1000 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --unshare-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  --ro-bind "$dir" /workspace --chdir /workspace \
  --setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  --setenv HOME /home/worker \
  -- \
  /home/worker/paddleocr/ocr.py "$1" "$file"

kill $child_pid

这个脚本会把指定文件所在的目录挂载到 chroot 内部,然后对着这个文件调用 PaddleOCR 来识别并通过返回结果。这个调用 PaddleOCR 的 ocr.py 脚本位于我的 paddleocr-web 项目

不过这也太复杂了。后来我又使用 systemd 做了个服务,简单多了:

[Unit]
Description=PaddleOCR HTTP service

[Service]
Type=exec
RootDirectory=/var/lib/machines/lxc-debian/
ExecStart=/home/lilydjwg/PaddleOCR/paddleocr-http --loglevel=warn -j 2
Restart=on-failure
RestartSec=5s

User=1000
NoNewPrivileges=true
PrivateTmp=true
CapabilityBoundingSet=
IPAddressAllow=localhost
IPAddressDeny=any
SocketBindAllow=tcp:端口号
SocketBindDeny=any
SystemCallArchitectures=native
SystemCallFilter=~connect

[Install]
WantedBy=multi-user.target

这里的「paddleocr-http」脚本就是 paddleocr-web 里那个「server.py」。

但它的防护力也差了一些。首先这里只限制了它只能访问本地网络,TCP 方面只允许它绑定指定的端口、不允许调用 connect 系统调用,但是它依旧能向本地发送 UDP 包。其次运行这个进程的用户就是我自己的用户,虽然被 chroot 到了容器里应该出不来。嗯,我大概应该给它换个用户,比如 uid 1500,应该能起到跟 subuid 差不多的效果。

顺便提一句,这个 PaddleOCR 说的是支持那么多种语言,但实际上只有简体中文等少数语言支持得好(繁体都不怎么样),别的语言甚至连语言名和缩写都弄错,越南语识别出来附加符号几乎全军覆没。

Category: Linux | Tags: linux 安全 隐私 网络
8
10
2022
2

tmux 状态栏优化

在 tmux 的状态栏里,通常会显示当前时间。配置起来也非常简单,%Y-%m-%d %H:%M:%S这样的时间格式化字符串扔过去就可以了。然而这样做有个小问题:这个时间只能精确到秒。我的意思不是说我想让它显示毫秒,而是希望它像电视台和广播电台的时间一样,显示(播报)「12:00:00」的时候,就刚好是这一秒的开始。

一般来说,这么延迟个一秒以内的随机数问题不大,除了你有多个这种时间戳的时候——

tmux inside tmux inside tmux

这些时间戳哪个先更新、哪个后更新可完全说不准的,你可能看到明明在地球另一边的服务器上先到某一秒,本地才跟上。甚至同一个 tmux 的不同客户端里,这个时间戳的更新时间都可能会有差异。

我想优化这个的另一个原因是,我经常使用 extrace 来查看程序调用另一程序使用的命令行参数,然而我本地连了多少个 tmux,每秒便会有多少个 sh + awk 进程出来读系统负载。尤其是我从 Awesome 换到 Wayfire 之后,顶栏改用 waybar 了,很多指示器都是内建或者自己写的外部脚本,不再需要每隔几秒跑个子进程去获取信息,这样 tmux 调用子进程来刷新状态造成的干扰就突显了出来。

于是就有了 accurate-time 程序。它每个整秒会去读系统负载,然后和当前时间一起送给 tmux 来显示。每秒一个进程,已经少了很多啦。

既然是我的程序自己来读负载,也就方便做更多事情了,比如根据负载情况使用不同的文字颜色:绿色表示低负载,灰白是稍微有点活干,蓝色和 cyan 是比较忙碌,黄色、品红表示已经忙不过来啦,红色就是要累趴下啦。之前偶然间发现 qemu-git 这个包使用 ninja、但是链接的时候又套了一层 make,造成系统负载冲到了两百多。但是无论高低,tmux 的负载显示都是红色,所以我可能之前已经视而不见许多次了。加上颜色之后,这类异常就更容易被注意到了。以前我本地每次风扇呼呼地转才发现系统负载高,但是我要是用耳机的话就听不到了,现在也多了个高负载的指示。

安装和配置很简单,cargo build --release 编译,然后把编译出来的 target/release/accurate-time 扔到 $PATH 里,再如下配置 tmux 状态栏右边即可:

if-shell "accurate-time tmux" {
  set -g status-interval 0
} {
  set -g status-interval 1
  set -g status-right "#[fg=red]#(awk '{print $1, $2, $3}' /proc/loadavg) #[fg=colour15]%Y-%m-%d %H:%M:%S"
}

本来我还打算给 waybar 上的时间也这么做一下的,不过程序写好了才发现 waybar 自己已经把时间对齐到更新间隔了。

Category: Linux | Tags: tmux Rust linux
8
8
2022
4

Google Chrome 中的字体设置

Google Chrome 是我的备用浏览器,主要用于对比和检查网页的渲染效果,以及某些网页在火狐上可能不太正常,就用 Google Chrome 试试。

但 Google Chrome 有个非常令人恼火的问题:默认字体实在太难受了!

如果没有指明字体,那么 Google Chrome 默认使用 Times New Roman 字体。这是我十年前从 Windows 那边拷过来的字体,看上去细细软软的,很复古,不太适合屏幕显示。屏幕显示一般使用无衬线字体,但 Google Chrome 默认是衬线字体。好吧,那网页要是指明要 sans-serif 字体呢?Google Chrome 这次看上了 Arial,同样是一款古老的、来自于 Windows 的字体。

Google Chrome 默认使用 Windows 字体

行吧……反正我也不是多喜欢这两字体,就全部删掉好了!结果,呃,「自定义」??

Google Chrome 固执己见

实际上它们分别是「Liberation Serif」和「Liberation Sans」字体。这是我的系统上 fc-match「Times New Roman」和「Arial」给出的字体,由 ttf-liberation 包提供,google-chrome 包依赖(看 AUR 上的评论,这是因为 Google Chrome 的 PDF 渲染需要这个字体)。

Google Chrome 就是这么喜欢 Times New Roman 和 Arial,系统上不安装它也要找个替代品来用,就是不听用户通过 fontconfig 设置的默认字体。用户要想 Google Chrome 听点话,需要在「设置」->「外观」->「自定义字体」里像这样设置一下:

Google Chrome 使用 fontconfig 的设置方法

而火狐的话,用户不需要在这种犄角旮旯里设置:

火狐默认使用 fontconfig 的设置

可惜大部分用户都在用 Google Chrome 或其变种,所以制作网页上还是得手动指定一些现代点的字体。

Category: Linux | Tags: 字体 Google Chrome
4
1
2022
9

从 getmail6 到 offlineimap

起因

上个月收到这样一封邮件:

自 5 月 30 日起,您可能会无法再访问那些采用安全性较低的登录技术的应用

意思就是说,Google 觉得把密码直接交给邮件客户端,权限太大,不够安全。所以要用户改用基于 OAuth2 的认证方式,只给程序邮件相关的权限。哦,你说应用专属密码?要用那个必须得启用两步验证——也就是意味着遇到灾难的话,我无法从一无所有的状态开始恢复。

从 POP 到 IMAP

getmail6 只支持使用 XOAUTH2 认证的 IMAP 协议,并不在 POP 协议上支持这个(不知道是否有可能)。所以我得换 IMAP 协议了。

具体操作步骤在 getmail6 的示例配置中有写。简单来说就是自己去申请个桌面软件的 app 信息,然后给自己的用户添加试用权限,再通过 OAuth2 获取 refresh token 和 access token,就能登录了。getmail6 自带了个 getmail-gmail-xoauth-tokens 程序用来走 OAuth2 流程,不需要另外安装程序来处理的同时也可以给其它程序使用。

所以我的 msmtp 配置就不用麻烦了,改两行配置就好:

auth oauthbearer
passwordeval getmail-gmail-xoauth-tokens ~/.getmail/gmail/lilydjwg@gmail.com.json

但是呢,虽然邮件是收回来了,IMAP 和 POP 还是挺不一样的。POP 没有「文件夹」的概念,所有收到的邮件,不管我有没有在 Gmail 网页或者客户端上阅读、归档,不管它进了哪个标签(文件夹)(「垃圾邮件」除外),我都会收到,并且把收过的邮件标记为已读。

而通过 getmail6 使用 IMAP 收取,我能做的选择就是,要不要把收过的邮件标记为已读或者删掉(可在 Gmail 中设置为归档)。不管如何,getmail6 只会收到它运行时位于收件箱中的邮件。如果我选择标记为已读的话,那么已读邮件也不会被 getmail6 收到。所以标已读的话,我在别的地方看过的邮件不会被收到。删除的话会好点,收过的邮件归档了,还省得我手动去归档,但是在别的地方,收过的邮件和已处理的邮件没了区分。

所以不如上 offlineimap,完全同步好了。

从 getmail6 到 offlineimap

offlineimap 的配置就比较复杂了,一是要对文件夹名进行转码,二是我要设定只同步指定的文件夹:收件箱、Maillist 和垃圾邮件。要同步垃圾邮件的原因是,Gmail 经常把有用的邮件往里边扔。

[general]
accounts = gmail
maxsyncaccounts = 10
socktimeout = 60
pythonfile = ~/.offlineimap/offlineimap.py

[Account gmail]
localrepository = gmail-local
remoterepository = gmail-remote

[Repository gmail-local]
type = GmailMaildir
localfolders = ~/.Maildir
filename_use_mail_timestamp = no
nametrans = gmail_nametrans_local

[Repository gmail-remote]
type = Gmail
remoteuser = lilydjwg@gmail.com

sslcacertfile = /etc/ssl/cert.pem
ssl = yes
starttls = no

oauth2_client_id_eval = get_client_id("lilydjwg@gmail.com")
oauth2_client_secret_eval = get_client_secret("lilydjwg@gmail.com")
oauth2_access_token_eval = get_access_token("lilydjwg@gmail.com")

nametrans = gmail_nametrans_remote
folderfilter = gmail_folderfilter
import os
import json
import subprocess

_LOADED_DATA = {}

def _load_data(account):
  with open(os.path.expanduser(f'~/.getmail/gmail/{account}.json')) as f:
    _LOADED_DATA[account] = json.load(f)

def get_client_id(account):
  if account not in _LOADED_DATA:
    _load_data(account)
  return _LOADED_DATA[account]['client_id']

def get_client_secret(account):
  if account not in _LOADED_DATA:
    _load_data(account)
  return _LOADED_DATA[account]['client_secret']

def get_access_token(account):
  cmd = [
    'getmail-gmail-xoauth-tokens',
    os.path.expanduser(f'~/.getmail/gmail/{account}.json'),
  ]
  out = subprocess.check_output(cmd, text=True)
  return out

def gmail_nametrans_remote(foldername):
  foldername = foldername.removeprefix('[Gmail]/').encode('ascii').decode('imap4-utf-7')
  if foldername == '垃圾邮件':
    foldername = 'Spam'
  elif foldername == '草稿':
    foldername = 'Drafts'
  return foldername

def gmail_nametrans_local(foldername):
  if foldername == 'Spam':
    foldername = '[Gmail]/垃圾邮件'
  elif foldername == 'Drafts':
    foldername = '[Gmail]/草稿'
  return foldername.encode('imap4-utf-7').decode('ascii')

def gmail_folderfilter(foldername):
  foldername = foldername.encode('ascii').decode('imap4-utf-7')
  return foldername in [
    'INBOX', '[Gmail]/垃圾邮件', '[Gmail]/草稿',
    'Maillist',
  ]

然后在 Gmail 那边创建个过滤器,把来自邮件列表的邮件扔到「Maillist」文件夹里去。搜索「 (to:@googlegroups.com OR from:vim-dev-github@256bit.org OR to:@zsh.org)」并创建过滤器,选择操作「跳过收件箱、 应用标签“Maillist”」即可。注意以后在修改的时候直接修改「包含字词」字段即可,并且记得「OR」「AND」「NOT」之类的操作符需要改回大写。

这样做完之后还有个问题:一封邮件同步到 offlineimap 后,我在 mutt 里阅读并删掉了它。offlineimap 一看,哟,邮件没了,得在服务器上删掉。Gmail 根据我的设置,把从 IMAP 删除的邮件归档,但是它并没有选项来标记为已读。所以这封邮件最终会以未读的状态躺在「所有邮件」里。

于是我去 App Script 里写了个脚本,把这些邮件标记为已读:

function mark_as_read() {
  const threads = GmailApp.search('is:unread AND NOT (label:Maillist OR in:inbox)', 0, 30)
  for(const thread of threads) {
    Logger.log('Marking as read: %s', thread.getFirstMessageSubject())
    thread.markRead()
  }
}

手动运行一遍之后,就可以在左侧栏里给它设置个触发器定时跑啦。

新邮件提示

使用 offlineimap 之后,最大的问题变成了邮件散落在不同的账号下的不同文件夹,一个个过去翻看太低效了。所以我就给 zsh 设置了提醒:

mailpath=(
  ~/.Maildir/INBOX/new'?GMail has a new message.'
  ~/.Maildir/Spam/new'?GMail has a new spam.'
  ~/.Mail/inbox'?New local mails.'
)

问号前边是邮箱的路径,后边是提示信息。之前那个 mbox 格式的邮箱我还留着,用来收取来自本地 cron 的邮件。

一个小问题是,procmail 用不成了。不过现在各种无用的网站消息也少了,所以不需要通过 procmail 处理垃圾邮件了(新浪微博我没有使用邮件注册、LinkedIn 和 Twitter 消停了、网易和QQ邮箱不用了)。现在中文邮件列表也几乎没人用了,我也不用让程序去重写「回复:RE:回复:」这类糟糕的邮件标题和过滤掉自动回复了。

Category: Linux | Tags: linux 电子邮件 IMAP
3
8
2022
8

Qt 的字体渲染问题

GUI 程序我现在依然倾向于 GTK,因为虽然 Qt 拥有良好的跨平台性,但可能是太注重跨平台性了,在 Linux 平台上反而有一些水土不服的问题。

字体太多,支持太少

你可能觉得,系统上字体太少,所以经常会遇到不常见的字符无法显示的情况。然而对于 Qt 来说,字体越多,反而越容易遇到个别字符不能显示的情况。

这是我的 /etc/fonts/conf.d/66-qt.conf 中的一段。因为顺序的原因,我只能放到 /etc 下。除了针对 sans-serif 配置外,我也有同样的配置应用于 serif 和 monospace。

<fontconfig>
  <!-- Adjust font order for Qt applications -->
  <alias>
    <family>sans-serif</family>
    <prefer>
      <!-- 格拉哥里字母:Ⰽⱁⱀⱄⱅⰰⱀⱅⰹⱀ Ⰹⱍⰹⰳⱁⰲ -->
      <family>Noto Sans Glagolitic</family>
      <!-- 爪哇文:꧁   ꧂ -->
      <family>Noto Sans Javanese</family>
      <!-- 西夏文:𗷲𗒅 -->
      <family>Noto Serif Tangut</family>
      <!-- 埃及象形文字:𓁹 -->
      <family>Noto Sans Egyptian Hieroglyphs</family>
      <!-- 苏美尔楔形文字:𒆠𒂗𒂠 -->
      <family>Noto Sans Cuneiform</family>
      <!-- 中日韩统一表意文字扩展 C:𫚥 -->
      <family>HanaMinB</family>
      <!-- 拉让文:ꥃ -->
      <family>Noto Sans Rejang</family>
      <!-- 越南傣文:ꪀꪑ -->
      <family>Noto Sans Tai Viet</family>
      <!-- 切罗基文:ꮳꮧꮢ ᨣ -->
      <family>Noto Sans Cherokee</family>
      <!-- 老傣仂文:ᨣ -->
      <family>Noto Sans Tai Tham</family>
      <!-- 安纳托利亚象形文字:𔘓 -->
      <family>Noto Sans Anatolian Hieroglyphs</family>
      <!-- 马姆穆文补充:𖤍  -->
      <family>Noto Sans Bamum</family>
      <!-- 图标字体(PUA): -->
      <family>OperatorMonoSSmLig Nerd Font</family>
      <!-- 巴塔克文:ᯤ -->
      <family>Noto Sans Batak</family>
      <!-- 古北欧文:ᛋᛖᚱᚣᚨᛚᚳᚨᚾᛞᛚᛖ -->
      <family>Noto Sans Runic</family>
    </prefer>
  </alias>
</fontconfig>

这个配置的意思是,把这些字体的优先级提高一些。当使用 fontconfig 的程序要显示字符的时候,它会指定一个模式,匹配到一个字体列表。渲染文字的时候,就可以遍历这个列表,直到找到可以显示这个字符的字体,所以一般来说,只要系统上装了对应字符的字体,它就能显示出来。

但是 Qt 额外地需要这个配置,因为 Qt 只会检查列表中的前255项。而世界上的不同文字那么多,所以想要能够显示它们,就得有一堆字体。比如 noto-fonts 这个包里就有614个字体文件,远超 Qt 支持的数量。总有些奇奇怪怪的文字被网友用来当颜文字,或者挂在名字上彰显个性。不这么调整一下,Qt 遇到了就只能「吃豆腐」了。

空心豆腐

当一个字符显示不出来的时候,那么怎么显示好呢?一般会显示成某种方框。Pango火狐会将该字符的 Unicode 码点以十六进制的形式显示在方框里边,这样虽然不知道这个字符长什么样子,但至少知道它是哪个字符,也知道多块豆腐是不是同一字符,在不能复制字符本身的时候很有用。比如当它出现在求助者的截图里的时候,比如当它出现在不能复制的地方的时候。

然而 Qt 不这样做。管你什么字符,Qt 统一显示为空心方框。从视觉上完全无法知晓它到底是什么字符,要是复制不到的话,就别想弄明白你缺什么字体了。

PS: Matrix 客户端 fluffychat 的 Web 版,使用的是 Fluffy 图形界面库,即使在 Web 版,文字渲染依然完全是自己做的。不管浏览器的设置不管系统的设置,豆腐块是带叉号的方框,还不能选中,十分讨厌。

非 BMP 字符

所有使用 UTF-16 的平台(Java、JavaScript、Windows、Qt),外加 MySQL 容易遇到的一个问题:非 BMP 字符(也就是那些 U+FFFF 之后的字符)会被当作是两个字符处理。随着 emoji 的流行,大家应该都修了不少。然而,Qt 在展示非 BMP 字符的时候,你可以选中半个字符。如果不小心漏掉半个的话,复制出来的半个字符就会变成问号(还好不是 GBK 时代那样弄乱后续所有字符)。

font features

一些字体可以通过 fontconfig 设置 fontfeatures 属性来启用(或者禁用)一些特性,比如连字,带斜杠的 0,小型大写字母,居中的中文标点,等等。Pango 很早就支持了,火狐最近也支持了,但 Qt 那边依旧没啥动静。(感谢 Coelacanthus 的评论。)

Category: Linux | Tags: linux 字体 Qt

| Theme: Aeros 2.0 by TheBuckmaker.com