Arch Linux 星球

June 22, 2022

Leo Shen

Use ZED with msmtp on Debian

Install msmtp and its mta. 1 apt install msmtp msmtp-mta mailutils Edit /root/.msmtprc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 defaults auth on tls on tls_trust_file /etc/ssl/certs/ca-certificates.crt logfile ~/.msmtp.log aliases /etc/aliases account alerts host SMTP_ADDRESS port 587 from SENDER_EMAIL_ADDRESS user SMTP_ACCOUNT password SMTP_PASSWORD account default : alerts Edit /etc/zfs/zed.d/zed.rc: Change ZED_EMAIL_ADDR to your desired mailbox

June 22, 2022 01:33 PM

June 12, 2022

Lainme

在CentOS7上安装H3C的iNode客户端

最近公司的VPN系统切换到了H3C的iNode,虽然H3C的网站上有安装手册,但那个安装过程繁琐且毫无必要,还会把系统弄的乱乱的,所以这里记录一下我自己的安装过程。

首先需要获取Linux版的软件,一般而言公司买了H3C的服务,都有相应的软件的,可以找IT要。如果要直接从H3C网站下载,可以从 https://www.h3c.com/cn/Service/Document_Software/Software_Download/IP_Management/iNode/iNode_PC/ 下载最新版。下载需要登陆,用户名和密码可以用 https://zhiliao.h3c.com/Theme/details/124921 链接中提供的那个(竟然真的可以登陆,我当时都震惊了……)

解压缩后在Linux目录下找到需要的版本,比如X86-64架构的就是“iNodeManager_H3C_Linux64_7.30(E0585).tar.gz”,解压缩后得到“iNodeManager”目录,进入这个目录执行安装脚本(其实大多数情况下没有必要……除非你的系统没有装过任何QT软件和7z才需要),

tar -xzf iNodeManager_H3C_Linux64_7.30\(E0585\).tar.gz
cd iNodeManager
bash ./install64.sh

再执行“iNodeManager”程序,

./iNodeManager

这个程序是对客户端进行定制的,完成后会生成客户端的安装程序,可以根据自己的需要勾选里面的选项。由于我们公司只用到了SSLVPN功能,我就只勾选了这一个,其他都不选(避免不必要的麻烦),如图所示,

点击右下角的“Finish”后会弹出新的对话框,勾选“Generate customized client setup program”,再点击“OK”。

完成后退出,并在iNodeManager目录下找到生成的“iNodeSetup”目录,进入这个目录解压自己需要的客户端版本,比如“iNodeClient_Linux64_7.3 (E0585).tar.gz”,得到“iNodeClient”目录,

cd iNodeSetup/
tar -xzf iNodeClient_Linux64_7.3\ \(E0585\).tar.gz

将这个目录移动到/opt下并用root权限执行安装脚本

sudo mv iNodeClient/ /opt/
cd /opt/iNodeClient/
sudo ./install_64.sh

安装完后就可以在系统菜单里找到iNode客户端。CentOS7要能正常启动客户端需要安装libpng12,别的系统没有测试。

sudo yum install libpng12

启动客户端后点击“+”添加链接,因为之前只选择了SSLVPN,所以协议选择的步骤直接“Next”就可以

其他连接参数的输入和Windows版差不多,这里就不写了。

by lainme (lainme@undisclosed.example.com) at June 12, 2022 11:52 PM

June 06, 2022

Phoenix Nemo

修复 CentOS 'org.freedesktop.login1' timed out

又到了喜闻乐见帮人修系统的时间。

根据情况描述是 “stuck at ‘Starting switch root’”,初步判定是 initramfs 坏掉了。经过询问果不其然,某个特别爱没事儿就更新的家伙在更新的时候被数据中心的智障远程手按了电源¯(ツ)/¯

既然启动不能那就进 Rescue 呗。虽然 CentOS 这系统不怎么样,但是至少也算在原版 ISO 就提供了 Rescue 的选项,还能自动寻找根分区挂载。本来以为小事一桩,准备 chroot 的时候突然冒出来一句 “chroot: cannot run command `/bin/sh’: Exec format error” 就瞬间心里一顿$#%#$^$#@#%#

一般来讲,报错 “Exec format error” 代表二进制格式错误,例如在 32 位系统中执行 64 位二进制文件。但这不可能,首先同样是 x86_64 架构,安装的系统和 ISO 的系统也是同样的版本,磁盘也没有报错,为什么会出现这种问题?

更新时断电导致文件被写坏的可能性并不是没有,但奇怪的是执行 /bin/bash 也报一样的错误。能坏到这种程度怕不是 glibc 挂了…?但总之第一步是要能先进系统瞧瞧究竟,于是心一横,直接用 Live CD 的 /usr 覆盖磁盘上的对应目录。

1
~> cp -r /usr /mnt/sysimage/

然后再 chroot,果然成功进入系统,yum 命令也可以用了。既然是更新过程中断电,那应该先尝试修复未完成的更新。

1
2
3
4
~> yum-complete-transaction
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
No unfinished transactions left.

啊咧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
~> yum update
--> Finished Dependency Resolution
Error: Multilib version problems found. This often means that the root
cause is something else and multilib version checking is just
pointing out that there is a problem. Eg.:

1. You have an upgrade for libselinux which is missing some
dependency that another package requires. Yum is trying to
solve this by installing an older version of glibc of the
different architecture. If you exclude the bad architecture
yum will tell you what the root cause is (which package
requires what). You can try redoing the upgrade with
--exclude glibc.otherarch ... this should give you an error
message showing the root cause of the problem.

2. You have multiple architectures of glibc installed, but
yum can only see an upgrade for one of those arcitectures.
If you don't want/need both architectures anymore then you
can remove the one with the missing update and everything
will work.

3. You have duplicate versions of glibc installed already.
You can use "yum check" to get yum show these errors.

...you can also use --setopt=protected_multilib=false to remove
this checking, however this is almost never the correct thing to
do as something else is very likely to go wrong (often causing
much more problems).

Protected multilib versions: glibc..... != glibc.....
kbd... x86_64 != kbd ... x86_64

…还真是。

进一步了解到这个系统并没有安装 32 位包,那么这个 32 位的 glibc 是哪儿来的呢?yum check 无果,更何况还有一个奇怪的 kbd 包甚至跟 32 位毫无关系。看来坏的不轻,但是既然确定系统中没有 32 位包,那么可以继续莽到底了。

1
2
3
4
5
6
7
# 完成所有未更新的包
~> yum update --setopt=protected_multilib=false
# 为重装系统做准备
~> mkdir /root/tmp
~> cd /root/tmp
# 重装所有包
~> yum reinstall * --setopt=protected_multilib=false

一通操作猛如虎,第一次重装所有包时还会报错 ldconfig 一些库是空文件,第二次完整重装就看不到报错了。看来可行,果断重启,一堆 OK 之后看到了 login 提示。

1
2
3
4
5
6
7
8
localhost login: root

Login incorrect

Login incorrect


Login incorrect

嗯嗯嗯???我还没输入密码呢???

还没输入密码就报错,至少不是 PAM 的问题。在那之前呢?应该是 systemd-logind 了。幸运的是网络可以正常启动,SSH 能够正常登入系统。如果网络或者 SSH 也不行,那就只好再次进 rescue 了。

系统里很多本来应该有的底层服务没有启动,例如 systemd-logind。想查看这货的状态,结果报错:

1
Failed to activate service 'org.freedesktop.systemd1': timed out

咦?systemd 自己都没正常启动,看看日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
localhost dbus-daemon[1166]: Encountered error 'Error in file /etc/dbus-1/system.d/org.freedesktop.hostname1.conf, line 1, column 0: no element found
' while parsing '/etc/dbus-1/system.d/org.freedesktop.hostname1.conf'
localhost dbus-daemon[1166]: Encountered error 'Error in file /etc/dbus-1/system.d/org.freedesktop.import1.conf, line 1, column 0: no element found
' while parsing '/etc/dbus-1/system.d/org.freedesktop.import1.conf'
localhost dbus-daemon[1166]: Encountered error 'Error in file /etc/dbus-1/system.d/org.freedesktop.locale1.conf, line 1, column 0: no element found
' while parsing '/etc/dbus-1/system.d/org.freedesktop.locale1.conf'
localhost dbus-daemon[1166]: Encountered error 'Error in file /etc/dbus-1/system.d/org.freedesktop.login1.conf, line 1, column 0: no element found
' while parsing '/etc/dbus-1/system.d/org.freedesktop.login1.conf'
localhost dbus-daemon[1166]: Encountered error 'Error in file /etc/dbus-1/system.d/org.freedesktop.machine1.conf, line 1, column 0: no element found
' while parsing '/etc/dbus-1/system.d/org.freedesktop.machine1.conf'
localhost dbus-daemon[1166]: Encountered error 'Error in file /etc/dbus-1/system.d/org.freedesktop.systemd1.conf, line 1, column 0: no element found
' while parsing '/etc/dbus-1/system.d/org.freedesktop.systemd1.conf'
localhost dbus-daemon[1166]: Encountered error 'Error in file /etc/dbus-1/system.d/org.freedesktop.timedate1.conf, line 1, column 0: no element found
' while parsing '/etc/dbus-1/system.d/org.freedesktop.timedate1.conf'
...

病的不轻。这些文件内容消失了,重装包并不会覆盖它们。只好手动来覆盖了。

1
2
3
4
5
6
7
# 安装必要工具
~> yum install yum-utils rpm2cpio
# 获取并解包 rpm
~> cd /root/tmp
~> yumdownloader dbus systemd
~> rpm2cpio dbus-1.10.24-15.el7.x86_64.rpm | cpio -idmv
~> rpm2cpio systemd-219-78.el7_9.5.x86_64.rpm | cpio -idmv

于是得到了这些包的内容。手动将 etc/dbus-1 目录下的所有文件覆盖到文件系统,然后执行 kill 1

1
2
3
4
5
6
7
localhost systemd[1]: Received SIGTERM from PID 4332 (bash).
localhost systemd[1]: Reexecuting.
localhost systemd[1]: systemd 219 running in system mode. (+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ
localhost systemd[1]: Detected architecture x86-64.
localhost dbus[1166]: [system] Successfully activated service 'org.freedesktop.systemd1'
localhost systemd[1]: Started Login Service.
localhost systemd-logind[4723]: New seat seat0.

仔细检查一遍,各种服务都在正常启动了!

(๑•̀ㅂ•́)و✧

后记:别没事儿更新生产用系统。

后记 2:西方国家的数据中心远程手没有一个靠谱的。

又到了喜闻乐见帮人修系统的时间。

June 06, 2022 03:22 AM

June 03, 2022

Felix Yan

萌新的 PolarFire SoC Icicle Kit 初体验

这两天翻出来了去年代收的 PolarFire SoC Icicle Kit。因为隔壁的 FPGA 大佬们看不上这块板子,我打算尝试物尽其用一下,目标只是用板子上的 RISC-V 核启动 Arch Linux RISC-V 的 rootfs 测试(把它当作一块 SD 卡槽没有问题、并且带 PCIE 的 HiFive Unleashed 来用。隔壁嵌入式群的大佬们:买椟还珠!)。如此便开始了年轻人的 FPGA 初体验(可能还是不能算)。

噩梦的开始

一开始尝试的当然是最新版的 Yocto 镜像,毕竟这是“官方”的 Linux 镜像。结果刷完后立刻遇到了启动失败:

一开始我还以为是 SD 卡坏了。在多次尝试未果后……

当时的猜测是(不一定对),可能因为板子上 FPGA 部分(抱歉,我不知道专业的称呼)不够新,所以我打算刷一下 HSS。结果这成为了噩梦的开始。

可怕的“硬件”工具链

我最初参考的文档来自 U-boot:https://u-boot.readthedocs.io/en/latest/board/microchip/mpfs_icicle.html

这份文档可能已经颇为过时,里面编译 HSS 的部分从一开始就找不到名叫“icicle-kit-es”的 BOARD.

在我加上 mpfs- 前缀,并根据后续报错依次按照我的 CROSS 工具链目标修改了 PLATFORM_RISCV_ABI=lp64d PLATFORM_RISCV_ISA=rv64gc 之后,我遇到了第一个大魔王:SoftConsole

好在这个工具可以无需注册直接下载。

顺利安装完成后,按照要求设置 SC_INSTALL_DIR,我终于看到了……下个错误:缺少 fpgenprog。

由于不想安装完整的 Libero SoC(一个巨大的需要折腾 license 的开发工具集),我试图去下载这里提到的 Program Debug Tool。从文档上找到下载链接点开后,看到了这样的悲剧:

它没了。

后面提到的 Programming and Debug 工具下载也藏在了注册墙后面,我考虑转向其他思路试试,先暂时没有继续了。

(另:关于 Libero SoC 的安装会有多坑,可以参考这位受害者的体验: https://www.cnx-software.com/2021/10/25/installing-libero-soc-in-ubuntu-and-windows-10/

Arch 内核的陨落

由于目前 Arch Linux RISC-V 基本没有处理 bootloader 部分的工作,我打算先试试直接使用旧版 Yocto 自带的 U-boot 来启动 Arch 的内核、rootfs。由于不熟悉 U-boot 和这些嵌入式镜像格式,我先花了点时间学习 U-boot 的启动逻辑 和操作 uImage 的常用命令等。

Yocto 的镜像里有一个很小的 /boot 分区,里面有 boot.scr.uimg 脚本和 fitImage 镜像。我遇到的第一个问题是,这个 FAT16 的 /boot 分区装不下 Arch 的 kernel,而且把 FAT16 扩容并转换为 FAT32 是 libparted 等常用工具不支持的操作。

在艰难尝试了许多次之后,我确定了这个分区后面的那个类型为 BIOS boot 但 GParted 抱怨为损坏分区的分区可能存放的是 uboot 本体(或者含它的 HSS?尚未仔细研究)。因此向后扩容 /boot 的想法可能不大靠谱。我只好退而求其次,把第一个分区删掉,直接把最后的 rootfs 所在分区标记为 legacy_boot

然后我复制了 Unmatched 上的 extlinux.conf 并做了相应修改(内核版本、UUID 等)。第一次尝试启动失败在缺少了两个环境变量:

后来在大佬们的帮助下,我找到了设置它的办法。其实正确的值就来自上面的 boot.scr.uimg,设置前者为 ${ramdisk_addr_r},后者则只需要设置一个足够大的值。

然而这次 Arch 的内核挂在了无限循环刷屏 L2CACHE ERROR 上。

由于大佬觉得这个问题不好解决,我决定先暂时放弃内核,用 Yocto 的内核启动 Arch rootfs 测试。

用魔法打败魔法

在现在的情况下,直接用原版的 boot.scr.uimg 和 fitImage 组合至少存在这样的问题:

  • 1、Arch rootfs 期待一个 rw 的 /。默认的 ro 环境会导致启动后一大堆服务失败、SSH Host Key 未生成等奇怪的问题。
  • 2、默认的 root= 设置为了 SD 卡的第三个分区(/dev/mmcblk0p3)。但经过上面的操作,SD 卡上现在已经只剩两个分区了。

为了修改 cmdline 解决这些问题,我在 U-boot shell 和 uEnv.txt 里鼓捣了各种操作都没有成功。看起来这个内核是把 cmdline 写死编译进去了。

在不重新编译内核的情况下,怎么更改里面写死的 cmdline 呢?那当然是直接修改二进制了!

首先把 fitImage 拆开,用 dumpimage 工具提取出里面的内核和 fdt 文件。从输出中可以看到,内核是 gzip 压缩过的:

用 gzip 解压这个文件,然后用 vim 打开:

诶嘿,我们的 cmdline 找到了。直接修改为想要的值(分区号直接替换,然后换掉一个应该影响不那么大的参数写上 rw),并保持字符串总长度不变(填空格补齐)。一共有两处,做同样处理即可。

修改完后,直接把未压缩的内核、fdt 复制到 rootfs 内,然后添加一个 extlinux 启动项:

label yocto
        menu label Arch Linux with Yocto kernel
        linux /boot/yocto.kernel.patched
        fdt /boot/yocto.dtb

Bingo!成功启动。

至此,最初的目标有了一个最低限度的成果。

尾声/题外话

感谢 PLCT 实验室、TUNA、AOSC 等社区一直以来的帮助,肥猫现在可能从完全不懂嵌入式稍微进步了一点点。

没想到折腾这块难啃的板子会成为我荒废三年多的博客再次更新的契机。我的这一轮 Arch Linux RISC-V 移植项目从最初尝试至今也已经超过三年,而今年年底就是我进入 Arch 十周年了。希望自己下次写博客不要又是三年以后 😛

The post 萌新的 PolarFire SoC Icicle Kit 初体验 first appeared on Felix's Blog.

by Felix Yan at June 03, 2022 07:26 PM

May 12, 2022

中文社区新闻

撤销以 wireplumber 替代 pipewire-media-session

两天前, wireplumber 新的打包会提示替代 pipewire-media-session 包,因为后者作为 PipeWire 的会话管理器,它的上游已经停止更新不再会有新版本了。不幸的是这个步骤有些仓促。
我们的 pipewire 音频包(pipewire-alsa, pipewire-jackpipewire-pulse)也提供了设置让 media-session 激活 PipeWire 的音频特性。如果没有按照这些包并且没有这些配置,PipeWire还是能用来处理屏幕录制而不干扰 ALSA 或 PulseAudio 。
WirePlumber 无视了这个机制,会总是试图让 PipeWire 接管音频设备,意味着 PulseAudio 和单用 ALSA 的用户的音频坏了。
已经撤销了包替换的修改,但是我们仍然在试图寻找一个更好的方案迁移到 WirePlumber 。如果你目前不使用 PipeWire 的音频功能,并且系统中已经安装了 wireplumber,请重新安装 pipewire-media-session 包后重启,以恢复音频功能。

pacman -Syu pipewire-media-session

by farseerfc at May 12, 2022 11:29 PM

May 11, 2022

Lainme

在CentOS7上安装最新版WPS

最新版的WPS需要GLIBC2.18以上,而CentOS7的版本是2.17,因此需要自行编译一个新的GLIBC。之前在CentOS 7上安装Dropbox这篇文章中写过如何编译和使用新的GLIBC,但当时写的方法不是很完全,这里再写一个更完整的版本。

依然先编译GLIBC,但最后一定要安装locale相关文件,不然WPS无法打开带有中文文件名的文件。

wget https://ftp.gnu.org/gnu/glibc/glibc-2.27.tar.gz
tar xzvf glibc-2.27.tar.gz
cd glibc-2.27
mkdir build
cd build
../configure --prefix=/opt/glibc-2.27
sudo make
sudo make install
sudo make localedata/install-locales

编译完成后将系统的库都链接过来,一劳永逸

cd /opt/glibc-2.27
sudo mv lib lib64
sudo mkdir lib
cd lib
sudo ln -s ../lib64/* .
sudo ln -s /lib64/* .

然后安装WPS并用patchelf修改WPS的各个可执行文件

cd /opt/kingsoft/wps-office/office6
sudo patchelf --set-interpreter /opt/glibc-2.27/lib/ld-linux-x86-64.so.2 et
sudo patchelf --set-interpreter /opt/glibc-2.27/lib/ld-linux-x86-64.so.2 wpp
sudo patchelf --set-interpreter /opt/glibc-2.27/lib/ld-linux-x86-64.so.2 wps
sudo patchelf --set-interpreter /opt/glibc-2.27/lib/ld-linux-x86-64.so.2 wpscloudsvr
sudo patchelf --set-interpreter /opt/glibc-2.27/lib/ld-linux-x86-64.so.2 wpsd
sudo patchelf --set-interpreter /opt/glibc-2.27/lib/ld-linux-x86-64.so.2 wpsoffice
sudo patchelf --set-interpreter /opt/glibc-2.27/lib/ld-linux-x86-64.so.2 wpspdf

by lainme (lainme@undisclosed.example.com) at May 11, 2022 10:35 PM

May 09, 2022

Leo Shen

Have fun with ZFS: Maintenance and Error Recovery

Now we have a pool, running smoothly. Let's learn a bit about regular maintenance, and what to do when things go wrong. Monitoring pool health The simplest way to check pool status is to use zpool status to view the pool status manually. This may be sufficient for small scale, non-critical systems; but for critical systems, we will want to know something went wrong the instant such event happens.

May 09, 2022 05:21 PM

中文社区新闻

QEMU >= 7.0.0 修改了拆包方式

从 qemu 7.0.0 更新开始我们用元包(meta package)将它拆包成了更细分的打包。

  • qemu包现在是被元包 qemu-base, qemu-desktopqemu-full 提供的虚包。
  • 7.0.0之前的qemu包的功能现在被qemu-desktop代替。
  • qemu-headless包的功能现在被qemu-base代替。
  • qemu-arch-extra包和qemu-headless-arch-extra包的功能现在被qemu-emulators-full代替。
  • 元包qemu-full包提供所有 QEMU 相关的包(除了qemu-guest-agent)

by farseerfc at May 09, 2022 10:53 AM

May 07, 2022

Alynx Zhou

装了台 NAS

本来我是不打算装 NAS 的,甚至都把我的星际蜗牛关了,因为我觉得我又没有网络多人协作的需求,而且我醒着的时候我的台式机也醒着,我睡着了又不会用到网络存储。不过自从我买了相机开始拍照片录视频,存储空间就越来越紧张了,先是把我机箱里的硬盘从 2T 直接升级到 8T,又觉得没有冗余始终心慌慌。偏偏我现在这个机箱哪里都不错,就是机械硬盘位不太充裕。为了扩展存储空间,也只能装一个 NAS 了。

决定好装 NAS 之后比较难的就是选硬件了,我肯定不会买那些闭源拖拉机的,我要装 Linux。肯定排除掉星际蜗牛,因为我实在不放心用那个背板带四块硬盘。然后对这种低功耗的设备用那种主板和 CPU 集成的赛扬应该不错,还免了主动散热,问题是我不知道在哪里能买到。所以还是考虑普通零售的硬件,不过大部分零售的硬件都有点性能过剩了,挑来挑去挑出下面的一套配置,比较个人倾向,不建议大家直接拿过来用。

  • CPU:i3-10105T 645
  • 主板:七彩虹 B460I 579
  • 机箱:乔思伯 N1 669
  • 电源:银欣 SX500-LG 549
  • 散热:利民 AXP90-X47 139
  • 内存:光威 8G DDR4 2666 169x2
  • 网卡:EDUP PCI-E 2.5Gbps 网卡 89
  • SSD:闪迪 至尊高速 NVMe 256G 259
  • HDD:西数 HC550 16T 1498x4
  • HDD:希捷 酷鹰 2T 375
  • 扩展:乐扩 M.2 A+E Key 转 SATA 75

下面简单介绍一下为什么选这套。

首先装 NAS 第一个要决定的不是别的而是硬盘,毕竟你这设备就是拿来放硬盘的,因为台式机里面已经有 8T 的硬盘了,我也不想再经历换硬盘时候拷贝数据的痛苦,所以还是直接上了 16T 的。这种容量建议直接购买企业盘,不过也就和静音说拜拜了。没选希捷的银河系列不是因为别的,只是因为西数 HC 系列便宜点而已。选 4 块盘而不是 3 块的原因也很简单,我不会用 ZFS,肯定要用 btrfs,但是 btrfs 的 RAID5 有 bug,于是性能容量冗余的平衡点就是 RAID10 了,那就需要 4 块硬盘起步。

决定好硬盘之后就得选合适的机箱,毕竟你得能把硬盘装进去,不过我对大部分的 NAS 机箱都不满意,有些类似个人产品或者众筹的机箱比如什么宝藏盒TANK之类的看起来很能装,但是造型不伦不类,做工也一般般,然后可能迎广或者万由有一些成品 NAS 机箱看起来不错,但是基本上是高配蜗牛,而且我实在不想用 flex 电源。其实某种意义上我是先看中乔思伯 N1 这个机箱才决定装的 NAS,我对这个牌子其实没什么好感,但这个机箱实在是过于好看,而且可以装 SFX 或者 SFX-L 电源,还是有很多零售产品可以选的。

选好机箱以后就是机箱里能塞进什么硬件就塞什么硬件了,乔思伯 N1 和大部分 NAS 机箱一样都只能放 ITX 主板,但 ITX 主板可选的实在不多。首先我想要一个内置核显的 CPU,因为最简单的调试办法肯定是给机箱接上键盘鼠标显示器,也比较方便调整 BIOS,而 ITX 只有一个宝贵的 PCIE 插槽,再说 NAS 多半也没必要上个亮机卡耗电。而 AMD 那边 200GE 太弱了,Ryzen Pro 系列又太强了,实在不知道选什么比较好,而且也不容易买到最低配的 Ryzen Pro CPU,所以就选 Intel i3 了,正好还有带 T 的低功耗版本。同时 10 代的主板还比较好买,也就是说能买到新的。虽然这台机箱前面有 USB-C,不过实际上是和 USB 3.0 共享一个插座,所以也不需要主板有 USB-C 的插座。七彩虹这块 B460 的优点在于后面还是有一个标准的 USB-C 接口,以及能在京东自营买到。

SFX 电源虽然不如 ATX 电源好选,但是仍然比 flex 电源好选太多了,而且这台机器还可以放下 SFX-L 的电源,12cm 的风扇肯定比 4cm 要安静很多。于是我就在京东随便挑了一个观感不坏的牌子的全模组 SFX-L 电源里功率最低的。不过装这个机箱的时候最好注意一点,买的电源的 24pin 线最好不要是那种捆成圆柱型的,因为需要从主板边上极小的缝隙里拉过来,排线肯定比圆柱线要容易过,银欣这款是排线。当然你是定制模组线的有钱人当我没说。

这款机箱最高可以支持 70mm 的下压散热,所以其实完全可以买利民那款 12cm 的散热器,不过我觉得也无所谓了,我比较担心 12cm 的下压散热会导致在主板上装内存和 SSD 比较困难,9cm 的也够用了。

内存其实是随便选的,因为我不用 ZFS,其它文件系统对于 ECC 也没那么严格的依赖。然后 i3 和 B460 也不会支持更高的频率了。

考虑到我的台式机上有两个网口,分别是 1Gbps 和 2.5Gbps 的,我就在想可不可以把 NAS 和台式机通过 2.5Gbps 直接连起来(路由器什么时候普及 2.5Gbps 啊),这样速度基本上和访问本机的机械硬盘没有太大差距。但是主板厂家实在是太抠门了,本来 ITX 主板型号就不多,内置 2.5Gbps 网卡的就更少了。虽然让一个 2.5Gbps 的网卡占着 PCI-E x16 插槽实在是有点浪费,但也没别的选择了。至于万兆,考虑到要使用专门的线和专门的网卡,还得给台式机也装一个,算了吧,反正机械硬盘的阵列也跑不满万兆。

然后系统盘只要随便搞一个 NVMe 就可以了,反正安装一个 Arch Linux 不会占用多少空间。实际上比较头痛的是怎么接上所有的硬盘。这款机箱有 5 个在背板上的 3.5 寸硬盘位和一个单独的 2.5 寸硬盘位。大部分 ITX 主板都只有 4 个 SATA 接口,本来我觉得接 4 盘的阵列够了,但是后来我发现北邮人 pt 可以使用我的交大学校邮箱注册……要是挂 PT,我觉得还是单独放一块硬盘比较好,那接口就不够用了。比较靠谱的办法是买 HBA 卡把 PCI-E 转成 SATA,但是已经被 2.5 Gbps 网卡用掉了。我还没有发现同时有网卡和 SATA 的 PCI-E 扩展卡。还有一个方案是 M.2 转 5 个 SATA 的转接卡,其实倒也可以用一块 2.5 寸的 SSD 当作系统盘,然后解放出这个 M.2 接口,不过我实在不放心把 4 盘阵列放在这种转接卡上……而且我估计系统盘也是不能接在转接卡上的。当然你可以买一块有 2.5 Gbps 网卡的 ITX 主板,不过那基本只有 Z590 可选了,最便宜的华擎 Z590 ITX 也要 1400。所以我最后采用的方案是放弃掉无线网卡,反正 NAS 摆在路由器附近可以拉网线,然后把无线网卡下面那个 A+E Key 的 M.2 接口利用上。有这样的转接卡,可以转接出两个 SATA 口,虽然我只要一个就够了。

不过当我拿到转接卡的快递之后发现安装还是有点麻烦。最好的办法是拧下无线网卡之后发现那个保护罩可以拆掉一侧的面板,然后可以把无线网卡从保护罩上拿下来再换成转接卡。但是那个固定无线网卡的螺丝实在太紧了我拧不下来,只能放弃这个保护罩,反正转接卡应该也不会受到什么外力,就在我把转接卡插好打算开机之前,我发现还是有点问题……虽然 SATA 接口已经够矮了,但是里面那个用不上的 SATA 接口还是顶住了前面的音频接口,导致转接板是歪的。其实可以淘宝再买一个 M.2 A+E Key 的延长线,不过那样我纠结的就是把延长出来的转接卡固定在哪里了。不管了!反正我就要一个 SATA 口,用刀把里面的 SATA 口的顶端削掉吧!虽然削的很粗糙但是还是达到了效果,然后用一块胶带把那个口粘住,防止 SATA 接口里面的金属触点接触到音频口保护壳而短路,再把一块泡沫放到转接板后面顶住转接板,我觉得就差不多了。

直接装上是歪的:

1.jpg

“高个的 SATA 接口被我给锯了,比矮个的还矮!”:

2.jpg

“磨”改之后的效果,应该没问题吧:

3.jpg

最后就是绞尽脑汁把各种线塞到 ITX 机箱里面,实在是太痛苦了。对了,这个机箱自带的风扇接线不是很长,而它那个硬盘背板上的插针又是满速的,如果你要是想插到主板上进行调速,最好自备一根 4pin 风扇延长线。

剩下就没什么好说的了,毕竟我不打算在这里复述一遍 Arch Wiki。我对 btrfs 使用的参数是 -m raid1c3 -d raid10,然后在系统里设置了 samba 和 transmission 的daemon。最后附上一些装完的照片。

4.jpg

5.jpg

6.jpg

by Alynx Zhou (alynx.zhou@gmail.com) at May 07, 2022 09:49 AM

April 09, 2022

Leo Shen

Have fun with ZFS: Tuning

We now have a working ZFS pool. Now, we will do some tweaking so that our pool can perform as good as possible. Disable atime (access time) By default, ZFS records the latest access time of a file. In many circumstances, this isn't actually useful and negatively impact performance. If you're not using applications that rely on access time (notable example being some mail clients, which use atime to see if mail has been read), we can turn it off to reduce disk writes.

April 09, 2022 06:25 PM

April 05, 2022

Lainme

给手机设置受限的SFTP服务器

准备允许自己的手机通过SFTP读写服务器上的一些文件,但无论是密码登陆还是密钥登陆都不能让我很放心,所以做了一些额外的配置并且要求

  • 手机只能登陆SFTP不能登陆SSH,且只能读写指定的目录
  • 不能影响任何现有的功能,比如我桌面端的登陆

先给手机建立一个专门的目录,目录及上级目录都要是root所有(为chroot做准备)

sudo mkdir /home/mobile

然后将所有允许读写的目录都mount bind过来。比如目录/home/[用户名]/Documents

sudo mkdir /home/mobile/Documents
sudo chown [用户名]:[用户名] /home/mobile/Documents
sudo mount --bind /home/[用户名]/Documents /home/mobile/Documents

然后设置SSH服务器如下(只显示关键配置)

Port 22
Port 2333

Subsystem sftp internal-sftp

Match LocalPort 2333
        AllowGroups [用户名]
        AllowUsers [用户名]
	X11Forwarding no
	AllowTcpForwarding no	
        AuthorizedKeysFile /home/[用户名]/.ssh/authorized_keys_sftp
	ChrootDirectory /home/mobile/
	ForceCommand internal-sftp

这里除了原本的22端口额外设置了一个2333端口,并且在连接这个端口的时候用authorized_keys_sftp来验证,chroot到/home/mobile目录,并且只允许SFTP。

然后重启SSH服务器

sudo systemctl restart sshd

在防火墙上放行2333端口,并将一组密钥对的公钥放到/home/[用户名]/.ssh/authorized_keys_sftp,私钥放到手机连接SFTP的APP上就可以连接了。密钥也可以设置Passprase。

by lainme (lainme@undisclosed.example.com) at April 05, 2022 07:41 PM

April 04, 2022

Lainme

用Syncthing进行文件同步

Syncthing是点对点的开源文件同步软件,在Linux上只需要将软件下载下来解压缩执行就可以直接使用,但这样设备的发现和数据中继都是用的官方提供的服务器,如果想要信息和数据更为可控可以搭建自己的发现和中继服务器。另外,虽然syncthing不需要中心客户端,但为了能时刻保持数据的完整以及简化多设备之间的同步,最好还是能有一个中心客户端作为核心24小时在线。

如果完全采用中心客户端的形式(即所有从属客户端都只和中心客户端连接,相互之间不连接),并且中心客户端安装在服务器上,也可以不要发现和中继,直接用IP地址直连。

准备工作

以Debian/Ubuntu为例,可以直接添加官方APT源来安装。

添加APT源的key

curl -s https://syncthing.net/release-key.txt | sudo apt-key add -

添加APT源

echo "deb https://apt.syncthing.net/ syncthing stable" | sudo tee /etc/apt/sources.list.d/syncthing.list

更新一下

sudo apt update

发现服务器

安装发现服务器

sudo apt install syncthing-discosrv

默认的设置可以直接使用,所以激活和启用服务就行了

sudo systemctl enable stdiscosrv
sudo systemctl start stdiscosrv
sudo systemctl status stdiscosrv

服务启动后会打印出设备ID(用sudo systemctl status stdiscosrv就能看到),只需要在防火墙上打开8443端口,就能用

  https://[服务器IP]:8443/?id=[设备ID]

作为发现服务器使用了。

中继服务器

安装软件

sudo apt install syncthing-relaysrv

中继服务器需要修改一些配置,编辑/etc/default/syncthing-relaysrv,修改如下

# Default settings for syncthing-relaysrv (strelaysrv).
NAT=false

## Add Options here:
RELAYSRV_OPTS=-pools=""

这会禁用UPnP/NAT-PMP,同时将中继服务器设置为私有(不加入公共池)。

激活和启用服务

sudo systemctl enable strelaysrv
sudo systemctl start strelaysrv
sudo systemctl status strelaysrv

和之前类似,服务启动后会打印出中继服务器的完整地址,例如

relay://0.0.0.0:22067/?id=[设备ID]&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0

不过里面的IP是“0.0.0.0”,后面的参数一般也不需要。在客户端填中继服务器信息的时候只需要以下形式。当然,22067的端口也需要在防火墙上打开。

relay://[服务器IP]:22067/?id=[设备ID]

中心客户端

同样从APT源安装

sudo apt install syncthing

然后激活和启用

sudo systemctl enable syncthing@[用户名].service
sudo systemctl start syncthing@[用户名].service
sudo systemctl status syncthing@[用户名].service

这里的用户名最好不要用root,一般而言是你希望放文件的用户名,最好和你其他客户端的用户一致(对于Linux而言不一致可能有账户权限问题)。客户端启动后也会显示出设备ID,可以填入其他客户端中进行同步。同时最好能在防火墙和路由器上打开22000端口,以便能够直连。

客户端启动后需要在Web界面做一些配置。如果中心客户端是安装在服务器上的,可以用SSH隧道把客户端的端口绑定到本地,就能用本地浏览器打开设置了。绑定的方法是

ssh -L [本地想用的端口]:localhost:8384 [服务器地址]

本地端口一般不要是8384,否则会和本地的客户端冲突。绑定后在本地浏览器输入

http://localhost:[本地想用的端口]

就可以打开服务器上的客户端界面。

安装在服务器上的中心客户端因为必然可以直连,可以不做什么设置,禁用Global Discovery和Enable Relaying就行。

如果是其他机器,基本设置如下:

  • 激活Global Discovery,但将地址改成自己的。
  • 激活Enable Relaying,但将监听栏的地址改成
tcp://0.0.0.0:22000, [你自己的Relay地址]

从属客户端

在自己的其他设备上安装syncthing客户端,设置上和中心客户端一样,然后添加中心客户端。如果中心客户端是安装在服务器上的,添加的时候可以把地址从dynamic改成tcp://[服务器IP]:22000

by lainme (lainme@undisclosed.example.com) at April 04, 2022 03:47 PM

April 01, 2022

百合仙子

从 getmail6 到 offlineimap

本文来自依云's Blog,转载请注明。

起因

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

自 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:回复:」这类糟糕的邮件标题和过滤掉自动回复了。

后记

然后今天我又试了一下 POP + 账号密码登录,还是可以用的。难怪通知里要写个「可能」……而反倒是 OAuth2 的 token 经常失效,需要重新走浏览器点几下来授权。

by 依云 at April 01, 2022 06:29 AM

March 26, 2022

百合仙子

微信消息通知的困扰

本文来自依云's Blog,转载请注明。

一直以来,不得不用的微信以其糟糕的通知体验让我十分不爽。

在手机上,我使用的是 Google Play 商店里的微信。在电脑上,我使用的是通过 Wine 运行的 Windows 版本微信([archlinuxcn] 仓库里的 wine-wechat-setup 脚本可用于安装)。

消息通知不及时

这个问题是最近我的手机日渐陈旧之后我才注意到的。表现是,在一段时间(比如一两天)不使用微信之后,收到新的微信消息或者视频通话,可能会延迟几个小时收到通知。在 Android 通知日志中可以确认,收到通知的时间和消息在微信中展示的时间有数小时之差,并不是因为我没有及时看手机。

我的 Telegram 从来不这样丢消息,即使因为后台进程过多 Telegram 被杀之后,通知只会不能在其它端阅读之后被清除,而不会延迟那么久。而微信,即使它还在后台运行着,却经常占着资源不干活,何况消息通知本应走 FCM。

打开微信即清除所有通知

我有一条微信消息,但是我现在不方便立即回复(比如需要使用电脑而我正出门在外),所以我会让那条通知一直留着。其实以前版本的 Android 系统更方便,可以将通知延后一段时间,只是不能自动指定延后的时间比较遗憾,后来被移除真的太可惜了。

然后呢,比如我要进个超市,或者测个核酸,付个钱啥的,只好打开微信扫码呗。结果所有还未处理的通知全部不见了!等忙完当时的事情,回到家里的时候,我就不一定还记得我还有几条微信消息还没处理了。

多端不同步容易错过

我在用电脑的时候,如果不登录电脑版微信的话,那么我将不会注意到手机上的微信有新消息。那就登录电脑版吧,然后我去上个厕所吃个饭,不用电脑的时候又会错过消息。在电脑端登录的时候让手机上也显示消息通知?那样所有消息都要看两遍,而且消息多的时候还得仔细回想某条消息到底是不是已经处理了。

Telegram 的消息同步做得真好啊。你在哪端用,哪端先给你发通知。其它端的消息会晚几秒出现。一旦在任意端读取了消息,另外的端上的相应消息全部都会被取消掉,不会有消息重复的问题(不过最近好像 Android 上的通知取消变得不那么可靠了)。

手机不可离远

现在电脑版微信终于不需要天天在手机上确认登录了。只要我每两三天登录一次,就可以避免把手机扔去充电了、来到电脑前、又去找手机的麻烦事。——我一开始是这么以为的。直到我发现,我刚刚看到通知、正要处理的消息,并没有在电脑版微信里同步出来。

不知道为什么,微信跟电脑用户有仇似的。明明我有电脑了,不需要凑合于手机的小屏幕和戳戳戳的屏幕键盘了,微信还非得把手机给拉过来。

语音通话不支持耳机接听

某天,我因为沉迷于放置型游戏,把手机扔桌子上充上电让它自个儿玩,自己去睡觉了。第二天早上睡正酣的时候,来了个电话,我拿耳机给接了。然后需要通话的另一方不知道怎么想的,没有商量就发起了微信语音通话,这个耳机根本接不到……

我也尝试过让 Google 助理回拨电话,不过使用不熟练,并没有成功。你说我为什么不起床去接?我睡着被电话惊醒了,还没回过神来啊 QAQ。

by 依云 at March 26, 2022 09:55 AM

March 25, 2022

中文社区新闻

Keycloak 17.0.1-2 更新需要手动重新配置

keycloak包在17.0.1-2版本前跑了WildFly服务器。因为上游在正式迁移到Quarkus发布,Arch Linux 也将跟从这一决定。这意味着升级时需要一些手动干预。
配置文件需要从老的 .xml 格式更新到新的 /etc/keycloak/keycloak.conf
在更新之前,停止 keycloak 服务,升级包,并且在开启服务前迁移配置:

systemctl stop keycloak.service
pacman -Syu keycloak
# migrate configuration /etc/keycloak/keycloak.conf
systemctl start keycloak.service

参阅 Keycloak 的 迁移文档 和 Keycloak Quakus 服务器文档

by farseerfc at March 25, 2022 11:53 PM

March 23, 2022

Alynx Zhou

March 21, 2022

Alynx Zhou

基于 GitHub Issue 的前端评论框

造轮子是病,得治。

自从造了 前端博客搜索引擎 的轮子之后,我对自己的能力有了极大的信心,同时也掌握了一些有趣的用法,于是把枪口瞄准了下一个我看着不顺眼的地方——评论框。

(这标题怎么写的和毕业论文似的!)

轮子也不是白造的。

从我建站以来我的评论框就命途多舛,Disqus 虽然是最著名的评论系统,但是在国内访问不太顺畅。多说火了一段时间之后便关门大吉,HyperComments 则在我用了一段时间后发邮件提示要收费了,于是我之前的评论便华丽流失。而对于 Valine 这种基于 LeanCloud 的评论系统,我对 LeanCloud 不甚了解所以也不想尝试(而且 Valine 现在似乎转向闭源了,当初也许是个正确的决定)。然后赶上 Gitment 和 Gitalk 火了起来,大家意识到 GitHub Issue 正是个放评论的好地方。但是由于 Gitment 和 Gitalk 采用他们自己的服务器实现博客评论框提交——转发到 GitHub API 的实现,以及 采用的 OAuth App 权限过高,有人怀疑这不太安全,于是我也没太敢参与。后来遇到 comment.js 这个项目,它绕过了提交评论的问题——直接给一个到 GitHub Issue 的评论框的链接就可以了。于是我就开始用这个,至于什么 utterances 这种用 GitHub App 降低权限的评论系统,我也懒得尝试了。

但我为什么决定替换掉 comment.js 我已经记不清楚了,可能是为了对主题的显示有更好的掌握吧,毕竟它带有自己的 CSS 样式而且经常和我的冲突,也可能是因为它迟迟没提供像 Disqus 一样查找每篇文章评论数目的功能,不过它现在已经不再维护了,所以我也算是未雨绸缪。

事情本该很简单。

研究了一下原理其实并不是很难,首先就是从 GitHub 的 API 上 ajax 获取数据,然后操作 DOM 添进去就可以了,所以我就先阅读了 GitHub API 的文档,总之还算容易,只要先获取一个仓库的 issue 列表,然后按照某种方法在里面查找相关的 issue,如果没有就渲染一个到新建 issue 的链接,否则获取该 issue 的全部评论并显示就可以了。

查找 issue 的 key 也算容易,我给它做成了函数参数,然后在主题模板里填一个每个页面唯一的字符串就行了,比如文章标题,然后新生成 issue 页面时候就把这个作为 issue 的标题,这样查找 GitHub Issue 页面时候也比较容易看。然后 GitHub 把 PR 也视为 issue,这个不要紧,收到数组之后过滤一下就好了。而显示数目我琢磨了一下其实也没什么难的嘛!我都获取到所有 issue 的信息了。做一个新的函数,主题作者在页面上放置一系列空的元素并设置好 class 属性,然后同样地把每个页面唯一的字符串设置成这些元素的属性(Disqus 也是这样的),加载函数时候把 class 作为参数传进去,分别从 issue 列表里查找对应的标题就可以了。还有一个奇葩问题是 GitHub issue 的 comments 指的是除了顶楼以外的评论,但很显然看起来不是这样的,这个也简单,直接把顶楼加到数组里就成了嘛。剩下就是艰苦的在浏览器里刀耕火种写 JS 拼 HTML 字符串发 ajax 请求写嵌套回调(没有 async/await 也太痛苦了吧!。

一切正常工作了一段时间一后我发现不太对劲,怎么评论太多的时候显示不全呢?仔细查了一下发现 GitHub API 是强制分页的,也就是说不管你怎么设置,最多一次只能获取 100 条评论,默认则是 30 条,本来我不太想给博客加评论分页功能的,现在看来是 GitHub 被迫让我加啊。当然这个并不是最痛苦的,最痛苦的是它查找仓库的 issue 列表的时候也是强制分页的!这就麻烦了,还记得我们之前说要获取到列表之后查找标题吗?获取不到完整列表还怎么查找啊!

当然你可以说按顺序多查几页不就行了嘛,这就是它分页难受的地方了!ajax 是异步的啊亲!你不会想让我一个 for 循环几个 ajax 还指望优雅的等他们结束了再跑查找吧!我知道能用 Promise.all() 解决,但是由于我大发慈悲的支持使用 IE11 的用户(微软我&A%¥S&……省略一千字儒雅随和),所以我的函数是基于回调的,那也没什么办法。而且这样首先你得读一下仓库信息才能知道有多少个 open 的 issue(没错只能算 open 的不能算 close 的!所以在后面查 issue 的时候我也不得不筛选掉 close 的,不过这大概也许是个 feature?),然后自己计算有多少页。最后我只能造了一个不那么优雅的尾递归回调(反正就那个意思),不过它工作的不错,这样我就可以获取到全部的 issue 列表了。

然后后面其实还是差不多,至于评论分页又不一样了,既然 GitHub 只有分页 API,我也就半推半就啊不是将计就计吧。我才不要继续获取全部评论了,我也每次直接获取 GitHub 那边的一页就行了,每页个数则由主题作者传参进来。至于如何确定我要哪一页呢?和搜索功能一样,继续前端解析 query string 呗。根据 issue 评论总数计算一下有几页,然后生成几个链接放在页尾,每次加载时候解析一下参数确定当前页是哪个即可。当然,不要忘了 issue 顶楼不算评论,计算分页的时候也不要给它加进去!而且既然是这么分页,我也懒得把顶楼算在里面了(不然真的麻烦的要死啊后面分页和每页个数全乱了),所以假如你设置每页 10 个评论,那第一页其实有 11 个,别烦我,代码在那,不满意自己改……然后继续刀耕火种……

为了减轻负担,我没有实现太多的功能,比如时间戳我没有搞成什么几分钟几小时前,这种东西又不清晰又浪费带宽,我只搞了基于 marked 的 Markdown 渲染(必须的)和语法高亮,Markdown 渲染不是必须的,因为你可以 设置 header 让 GitHub 直接返回 HTML。为了保证效果统一,这个 JS 只是建立了 HTML 布局,给每个元素添加了 class,具体的样式则完全是主题自己编写的,所以配合起来也比较好看。

最后的效果其实还可以,完整的脚本就是 这个网站在用的 JS,具体和主题的整合方法就慢慢翻 ARIA 的模板吧。唯一的缺点是 GitHub API 的频率限制太低,按照这个弱智的 issue 列表分页的话又不得不多一次读取仓库有多少 issue 的请求,假如你的 issue 太多估计也是问题?不过应该不会有那么多博文吧!我只有调试脚本的时候遇到过被 GitHub 提示等会的问题,所以对于访问者应该没什么影响。有影响以后再想解决办法(或者没办法)。

也许最好的办法是解决掉需求——要什么评论框?不就是破事水?如果有问题想联系作者电子邮件又不是不能用!(逃

其实你知道,烦恼(bug)会解决烦恼(bug)。

更新(2022-03-21 16:46:46):我终于发现 `fetch` 不会主动帮我做缓存的原因是我一直习惯性的在 DevTools 里面勾上 Disable Cache……所以下面这些其实都没有必要了,只要取消勾掉 Disable Cache,调试的时候就不会很快撞到查询频率次数限制了……
这一部分更新于 2020-08-17 10:34:00。

GitHub API 推荐用户 缓存之前的请求响应,然后根据缓存的响应的 Header 里面的 ETag 发送请求查询是否过期,若未过期则返回一个不消耗频率限制次数的 304 状态码。我心想这也简单,那就在前端搞一个缓存就可以了。

然后我搜索了一番找到了 CacheStorage,看起来它是唯一一个跨标签页的基于 Session 的正宗的前端缓存。但是很显然 IE 又不支持,而且这个 API 基于 Promise 并且只能缓存 Response 对象,也就是说没办法简单的通过在 XHR 的时候判断一下跳过不支持的情况,要 Polyfill 则需要引入完整的 Promise 和 fetch/Response,所以我们做了一个艰难容易的决定——是时候去掉 IE 支持了!

于是我把请求 API 的函数改成了如下操作:

let cachePromise = window.caches.open("cacheName");

// Fetching JSON with cache for GitHub API.
const cachedFetchJSON = (path, opts = {}) => {
  let cachedResponse = null;
  return cachePromise.then((cache) => {
    return cache.match(path);
  }).then((response) => {
    // No cache or no ETag, just re-fetch;
    if (response == null || !response.headers.has("ETag")) {
      return window.fetch(path, opts);
    }
    // Ask GitHub API whether cache is outdated.
    cachedResponse = response;
    opts["headers"] = opts["headers"] || {};
    opts["headers"]["If-None-Match"] = cachedResponse.headers.get("ETag");
    return window.fetch(path, opts);
  }).then((response) => {
    if (response.status === 200) {
      // No cache or cache outdated and succeed.
      // Update cache.
      cachePromise.then((cache) => {
        return cache.put(path, response);
      });
      // Cache needs an unconsumed response,
      // so we clone respone before consume it.
      return response.clone().json();
    } else if (response.status === 304 && cachedResponse != null) {
      // Not modified so use cache.
      return cachedResponse.clone().json();
    } else {
      // fetch does not reject on HTTP error, so we do this manually.
      throw new Error("Unexpected HTTP status code " + response.status);
    }
  });
};

当然理想很丰满现实很骨感,在不支持 CacheStorage 的浏览器里要 fallback 到不带缓存的版本,本来我以为很简单,但是……(下面开启吐槽时间。)

支持 IE 的前端的痛苦都是相似的,不支持 IE 的前端则各有各的痛苦。

为什么非 HTTPS + localhost 不能用 CacheStorage 啊,难道他们没考虑过在电脑上开发然后手机访问测试移动版吗?还是说他们打算在手机上起一个开发服务器?为什么 Firefox 在非 HTTPS 时限制 CacheStorage 的方法是在 Promise 里 reject 一个 Error 从而导致这个过程变成了异步的?为什么 CacheStorage 只能缓存 Response 而不是任意数据结构?Safari 不能完整支持 Response 对象也就算了,为什么移动版 Chrome 和 Firefox 也不支持?合着你们 fetch 返回的 Response 还不是 Response?这世界到底怎么了……

所以最后需要一个长长的基于 Promise 的判断加载函数:

// 加载评论的时候才加载缓存。
let cachePromise = null;

let fetchJSON = uncachedFetchJSON;

const loadCache = (name) => {
  // Unlike in .then(),
  // we must explicit resolve and reject in a Promise's execuator.
  return new Promise((resolve, reject) => {
    if (cachePromise != null && fetchJSON !== uncachedFetchJSON) {
      return reject(new Error("Cache is already loaded!"));
    }
    // Old version browsers does not support Response.
    if (window.Response == null) {
      return reject(
        new Error("Old version browsers does not support Response.")
      );
    }
    const testResponse = new window.Response();
    // Safari and most mobile browsers do not support `Response.clone()`.
    if (testResponse.headers == null || testResponse.clone == null) {
      return reject(new Error(
        "Safari and most mobile browsers do not support `Response.clone()`."
      ));
    }
    // Chromium and Safari set `window.caches` to `undefined` if not HTTPS.
    if (window.caches == null) {
      return reject(new Error(
        "Chromium and Safari set `window.caches` to `undefined` if not HTTPS."
      ));
    }
    window.caches.open("CacheStorageTest").then((cache) => {
      fetchJSON = cachedFetchJSON;
      cachePromise = window.caches.open(name);
      return window.caches.delete("CacheStorageTest");
    }).then(() => {
      return resolve();
    }).catch((error) => {
      // Firefox throws `SecurityError` if not HTTPS.
      console.error(error);
      return reject(new Error("Firefox throws `SecurityError` if not HTTPS."));
    });
  }).catch((error) => {
    console.error(error);
  });
};

不管怎么样现在这个网站在支持 CacheStorage 和 Response 的浏览器上(似乎也就桌面版 Chrome/Firefox……)是缓存 GitHub API 的结果了,打开 DevTools 切到 Network 面板可以看到 GitHub API 返回的是 304 而不是 200,其他浏览器则 fallback 到无缓存的 fetch。当然其他浏览器不包含 IE 咯。

由俭入奢易,由奢入俭难。

这一部分更新于 2020-08-18 12:25:00。

我后来又仔细想了想,其实要兼容 IE 还是有办法的,首先 fetchPromise 都有成熟的 polyfill,甚至 URLSearchParams 也有,只要写一段脚本在不支持的时候加载他们就可以了。然后去掉所有 IE 不支持的 ES6 特性,比如箭头函数、模板字符串、for…of… 循环以及 MapReduce(IE 竟然支持 constlet 真是惊到我了)。但是能做到并不意味着一定要做,人总是还要向前看的,现在是 2020 年,连罪魁祸首始作俑者微软都放弃了 IE,就算是照顾用户量,IE 用户也是可以忽略的那一部分了。既然我已经用 ES6 重写了,就不要想再让我为这种历史垃圾放弃我得到的好处了,从我开始写主题到现在丢掉 IE 支持也算是仁至义尽了,所以为什么不让这些用户支持一下 Firefox 呢?

by Alynx Zhou (alynx.zhou@gmail.com) at March 21, 2022 08:46 AM

March 18, 2022

Leo Shen

Have fun with ZFS: Setting up storage pool

Now we've explored the benefits and limitations of ZFS, let's create a pool and try it out! This guide will not explore how to use ZFS as root, as it is very distro/OS-dependent. View guides about this topic on your distro/OS's own Wiki or Handbook. Concepts Unlike filesystems that operates on a single disk, partition or logical volume (i.e. ext4 and NTFS), ZFS can span across multiple storage volumes.

March 18, 2022 03:49 PM

March 08, 2022

中文社区新闻

Arch Linux 项目负责人竞选结果

本月我们举行了我们的负责人竞选,结果是没有对我们的当前负责人 Levente Polyák 的异议。根据我们的 选举规则,他将继续连任一个新任期。

恭喜 Levente Polyak 赢得新任期!

by farseerfc at March 08, 2022 02:05 PM

百合仙子

Qt 的字体渲染问题

本文来自依云's Blog,转载请注明。

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 的评论。)

by 依云 at March 08, 2022 08:45 AM

March 07, 2022

Alynx Zhou

我如何在 Emacs 里面处理缩进宽度和 Tab 宽度

Tabs are 8 characters, and thus indentations are also 8 characters. There are heretic movements that try to make indentations 4 (or even 2!) characters deep, and that is akin to trying to define the value of PI to be 3. -- Linux Kernel Coding Style

当然,我不是在任何情况下都同意上面那句话,虽然在写 C 的时候它是绝对的真理。我个人倾向于取其中一部分——任何试图把 Tab 宽度定义为 8 以外的行为都无异于把 PI 定义为 3。

写这篇文章是因为我意识到一个问题:Tab 宽度和缩进宽度是两个无关的变量。很多人简单地把缩进宽度理解为 Tab 宽度,不过这不能怪他们,因为大部分的编辑器都把这两者当作相同的东西。而当我重新开始用 Emacs,才发现这两个本来应该是不一样的。

为了防止迷惑,首先解释几个我说的名词:

  • Tab 宽度:字面意义上的一个 Tab 字符应该相当于几个空格的宽度,在 Emacs 里面是 tab-width 这个变量,我个人建议固定为 8。
  • 缩进宽度:当你的代码应该增加一个缩进级别的时候,应该向右缩进几个空格的宽度,在 Emacs 里面每个模式都有不同的变量控制,比如 c-indent-offset
  • 缩进级别:代码逻辑层次(或许可以简单理解为作用域)每增加一层,一般就应该增加一个缩进层级。
  • 使用 Tab 缩进:不是说只使用 Tab 缩进的意思。事实上如果你的代码可以只使用 Tab 缩进,那么 Tab 宽度为几都无所谓,因为一个缩进宽度就是一个 Tab,但实际上你不一定只使用 Tab 缩进,因为你可能还包含对齐的情况,比方说函数头参数太多,而你希望换行后的参数能够和左括号的下一个字符对齐,那么这个距离大概率无法被 Tab 宽度整除。因此使用 Tab 缩进的含义是“总缩进宽度能够用 Tab 宽度整除的部分用 Tab,余数的部分用空格”,也就是 Emacs 里面 (indent-tabs-mode 1) 的效果。
  • 使用空格缩进:和上面相反,意味着缩进部分完全不使用 Tab 字符,那么 Tab 宽度为几也无所谓。也就是 Emacs 里面 (indent-tabs-mode -1) 的效果。

我个人的建议和倾向主要是两个,第一个是 Tab 宽度固定为 8,第二个则是如果你认为缩进宽度为 8 也就是一个 Tab 对你来说太宽,那么你就应该完全不用 Tab 缩进,也就是使用空格缩进,而不是修改 Tab 宽度。

这样做的理由很简单,主要是我最近修改 GTK 代码时候发现的,GTK 代码是典型的 GNU 风格,我一开始以为它是完全不使用 Tab 缩进,并且缩进宽度是 2,并且我在 Atom 里面也是把这个项目的 Tab 宽度设为 2,但我前段时间发现 GTK 实际上是使用 Tab 的,比如一个很有意思的情况:某段代码的总缩进级别是 5,而它实际上使用的是 1 个 Tab 和 2 个空格,如果你把 Tab 宽度设置成 2,那这段代码在你的编辑器里就会错误的显示成 2 个缩进级别,只有你的 Tab 宽度是 8 的时候才能正确的显示。

这些奇怪缩进的项目给了我两个教训:Tab 宽度和缩进宽度并不是一个东西,以及最好假设 Tab 宽度为 8。当然,以上的建议都是以遵照现有代码的风格为前提,应该不会有人任性到把别人的项目都改成自己风格再提交贡献吧。

我倒不是完全的 Tab 缩进党,比如在 Python 里我设置缩进宽度是 4,而在 JavaScript 里我设置缩进宽度是 2,主要是因为对于 C 这种不允许嵌套函数并且没有类似 class 这种层次的语言来说,缩进级别完全就是函数内部逻辑,当你总缩进宽度达到 24 的时候,你已经在函数里有三层逻辑了。而对 Python 或者 JS 这类函数经常是类的方法的语言,本身函数就已经带着一个缩进级别了,三个缩进级别仅仅只代表两层函数内部逻辑,更别提比如回调函数是匿名函数的情况了。

至于当缩进宽度不是 8 时使用空格缩进而不是用 Tab 然后修改 Tab 宽度的理由也很简单,和上面说的一样,完全使用 Tab 缩进时虽然 Tab 宽度不会影响总缩进级别,但是一旦遇到对齐的情况,Tab 宽度不一致,对齐的部分就不再一样了。

说了这么多理论,该到具体的我怎么在 Emacs 里面处理了,首先是默认值,我个人是定义 Tab 宽度为 8 并且设置允许使用 Tab 缩进。

(setq-default tab-width 8)
(setq-default indent-tabs-mode t)

默认的 C 编码风格是 GNU,但是 GNU 的编码风格实在是太恐怖了,特别是大部分人的入门书上应该都是 K&R 或者类似的风格。我换成了我喜欢的 linux 内核风格。有的变量你可以直接使用 setq,但另一些使用 setq 会说你声明了一个新的自由变量,这个时候还是遵照建议使用 Emacs 的 customize 系统吧。

(customize-set-variable 'c-default-style '((java-mode . "java")
                                           (awk-mode . "awk")
                                           (other . "linux")))

前面说过 Emacs 对于不同的模式使用不同的变量作为缩进宽度,这样对于编写配置其实很困难,因为让你时刻记住哪个模式用哪个变量显然不太现实,这里我用了一个比较投机取巧的办法——Emacs 有所谓 buffer-local 变量的设定,也就是说一个变量会有一个默认值,然后每个 buffer 都可以有一个该变量的副本,可以设置成不同的值,如果没有则使用默认值。利用这个功能我创建了一个单独的 buffer-local 变量 indent-offset,然后把以上所有这些都设置为该变量的别名,于是我对每个 buffer 只要修改 indent-offset 的副本就可以了。我这里只写了我使用到的模式的变量,如果有其他的就加到列表里,或者我看 doom-modeline 的代码里几乎包含了所有常见模式的变量,或许可以拿来用。

(defconst mode-indent-offsets '(c-basic-offset
                                js-indent-level
                                css-indent-offset
                                sgml-basic-offset
                                python-indent-offset
                                lua-indent-level
                                web-mode-code-indent-offset
                                web-mode-css-indent-offset
                                web-mode-markup-indent-offset
                                markdown-list-indent-width)
  "Different modes' indent variables to make alias to indent-offset.")

(dolist (mode-indent-offset mode-indent-offsets)
  (defvaralias mode-indent-offset 'indent-offset))

(defvar-local indent-offset tab-width)

那么接下来就是我们如何针对不同的 buffer 进行不同的设定了,这里分为两部分,一部分是这个 buffer 是否使用 Tab 缩进,另一部分则是缩进宽度设置为多少。Atom 里面有一个状态栏插件,点击就可以快速设置,Emacs 里面我写了两个简单的函数方便调用,执行对应的函数设置是否使用 Tab,并且它们都会询问你想把缩进宽度设置为多少。我分别把这两个函数绑定到 C-c i TABC-c i SPC。对于 GTK,操作起来可以是 M-x indent-tabs RET 2 RET

(defun indent-tabs (num)
  "Mark this buffer to indent with tabs and set indent offset to NUM chars."
  (interactive `(,(read-number "Indent offset (chars): " indent-offset)))
  (indent-tabs-mode 1)
  (when (/= indent-offset num)
    (setq indent-offset num)))
(global-set-key (kbd "C-c i TAB") 'indent-tabs)

(defun indent-spaces (num)
  "Mark this buffer to indent with spaces and set indent offset to NUM chars."
  (interactive `(,(read-number "Indent offset (chars): " indent-offset)))
  (indent-tabs-mode -1)
  (when (/= indent-offset num)
    (setq indent-offset num)))
(global-set-key (kbd "C-c i SPC") 'indent-spaces)

当然以防万一你真的遇到一个脾气古怪的作者,一定要使用 Tab 缩进并修改 Tab 宽度,我也写了个修改 Tab 宽度的函数。这个我绑定到 C-c i w 了。

(defun set-tab-width (num)
  "Mark this buffer to set tab width to NUM chars."
  (interactive `(,(read-number "Tab width (chars): " tab-width)))
  (when (/= tab-width num)
    (setq tab-width num)))
(global-set-key (kbd "C-c i w") 'set-tab-width)

现在我们有了给每个 buffer 修改设定的办法了,但是通常你对某种语言有一个自己偏爱的风格,肯定希望以这个为默认值,所以我写了一些代码,给每个模式设置成我喜欢的默认风格。同样地如果你装了更多的模式,或者和我有不同的喜好,就修改这些列表好了。

这里使用 (set-tab-width 8) 是因为有些模式比如 markdown-mode 把 tab-width 定义为 4,按照前面说的,我觉得这是错的,同时上游为了保持向前兼容不好修改,于是这里简单地覆盖掉。

(defconst indent-tabs-modes '((prog-mode . 8)
                              ;; `markdown-mode` is not a `prog-mode`.
                              (markdown-mode . 8)
                              (gfm-mode . 8))
  "Modes that will use tabs to indent.")

(defconst indent-spaces-modes '((lisp-mode . 2)
                                (emacs-lisp-mode . 2)
                                (js-mode . 2)
                                (css-mode . 2)
                                (html-mode . 2)
                                (yaml-mode . 2)
                                (lua-mode . 3)
                                (python-mode . 4))
  "Modes that will use spaces to indent.")

(dolist (pair indent-tabs-modes)
  (add-hook (intern (concat (symbol-name (car pair)) "-hook"))
            `(lambda () (indent-tabs ,(cdr pair)) (set-tab-width 8))))

(dolist (pair indent-spaces-modes)
    (add-hook (intern (concat (symbol-name (car pair)) "-hook"))
              `(lambda () (indent-spaces ,(cdr pair)) (set-tab-width 8))))

有些 modeline 包含一个显示缩进信息的部分,比如 doom-modeline 显示 TAB 或者 SPC 表示使用 Tab 缩进或使用空格缩进,然后如果该模式有自己的缩进宽度变量就显示,没有就显示 Tab 宽度(我没仔细读,总之它对这俩不做显式区分)。而按照上文,我肯定是倾向显式区分这两个东西的,所以我们不用它的,而是自定义一段 modeline,第一部分显示 TABSPC,第二段显示 indent-offset,第三段显示 tab-width。我没太搞懂 Emacs 的 modeline constructor 的语法,我觉得我写对了但却没有,于是最后变成 :eval 一个 format 调用了。

(setq mode-line-misc-info '(:eval (format "%s %d %d"
                                          (if indent-tabs-mode "TAB" "SPC")
                                          indent-offset
                                          tab-width)))

最后我在网上抄了两个配置,一个是让它按回车时候不要自动缩进。另一个是修改默认的删除缩进的行为,默认当你在对着一个 Tab 按下退格键的时候,Emacs 把这个 Tab 变成缩进宽度数量的空格,然后删掉一个空格,这太诡异了,我就让它删掉一个字符好了。

(setq-default electric-indent-inhibit t)
(setq backward-delete-char-untabify-method nil)

这样当你遇到一个和自己习惯不一样的文件,基本只要看情况调用 indent-tabs 或者 indent-spaces 即可。我在 Atom 用的插件还有一个自动猜测文件是使用 Tab 还是几个空格作为缩进的功能,不过我看了一下代码,它并不能解决我之前说到的 GTK 的问题,也就是说它会猜成 SPC 2 2 而不是 TAB 2 8,我懒得自己想一个猜测算法,于是就还是靠自己判断了。

一个我比较想实现的功能是记录每个目录我用了什么设定,因为基本上每个项目都用一样的风格,这样就不用每次编辑文件都手动设置。我知道可以在每个目录创建一个文件记录 Emacs 的一些目录范围变量,但是问题是不是所有项目都想让你添加一个编辑器相关的文件,甚至你都不好修改 .gitignore 排除你的文件。我比较想把这个存储记录丢到 Emacs 的目录里,不过并不知道怎么实现,如果哪天我搞清楚了,就去写一个。

by Alynx Zhou (alynx.zhou@gmail.com) at March 07, 2022 01:11 AM

February 26, 2022

Alynx Zhou

Emacs 和 Monaco 字体和 Box-drawing Character

2016 年的我开始用 Atom 这种“modern”的编辑器,2022 年的我却又开始用回岁数比我都大的 GNU Emacs。切换的理由其实很简单,我曾经以为一直能追上最新版 Electron 的 VSCode 会成为第一个纯 Wayland 的代码编辑器——只要 Chromium 那边支持纯 Wayland 就好了嘛,然而直到 Emacs 那边的 pgtk 分支合并进主线(以防有读者不太清楚来龙去脉我解释一下,Emacs 虽然有图形界面,但实际上只是用 X 实现了一个 Terminal 层,而传统的 GTK3 界面只是使用 GTK3 创建一个 X 窗口,然后其它操作都是通过 X 进行,这实际上非常不适合 GTK3,导致了很多 bug,同时也使 Emacs 没法利用 GTK 的 Wayland 后端。而 pgtk 分支则是在 X 部分之外另起炉灶,利用 GTK 实现了一个和 X 部分平行的 Terminal 层,全部的绘制操作都是以 GTK/Cairo 的现代程序方式进行,自然也就摆脱了对 X 的依赖。总之在 Emacs 这样又老又庞大的代码库上做如此大范围的工程我觉得可以称得上是一项壮举了。),Chromium 的 ozone backend 还是问题多多。虽然 Emacs/Vim 这种软件看起来确实有点老派作风,但没想到也有走在这些“现代”编辑器前面的地方。

至于这和我换掉 Atom 有什么联系呢?主要是我发现在家里的台式机上,所有 XWayland 程序在 nvidia 驱动下面都会有闪回的情况,也就是说你打字的时候突然会闪回前几帧的画面,过一会再闪回来,你经常看不到自己输入的字符。Electron 程序尤其严重,也就导致我没有办法使用 Atom 写代码,于是不得不捡起以前东拼西凑的 Emacs 配置重新研究。(奇怪的是我自己的台式机也是 nvidia 驱动,没遇到过这种问题。)

扯远了,这篇文章主要想记录的问题是什么呢?其实还要和 Emacs 绘制界面的方式有关系,对于 Atom/VSCode 这种基于浏览器的程序来说绘制点什么图形元素很简单,但是对于 Emacs/Vim 这种来自于终端里的程序,开发者们习惯的是处理字符而不是处理图形,于是你会发现比如 80 column ruler 或者 indent guide 这种东西,在 Emacs 里面其实是通过在对应的位置插入竖线字符实现的……我个人不太喜欢这样,一个原因是我以为竖线字符并不是占满整行而是上下有空白。我一直以为这是 Emacs 的问题——你干嘛用竖线字符画 UI 啊。

有问题的样子

直到有一天我输错了 alias 在 GNOME Terminal 里面打开了 Emacs,我惊讶的发现竖线竟然接上了头!

终端里的样子

我当时就震惊了,我的终端和 Emacs 用的是同样的 Monaco 字体,怎么会不一样呢?难道是 Monaco 字体有问题?于是我上网搜了一下,我以前一直以为是 Emacs 的哪个设置比如 line-spacing 我没搞好,怎么搜也搜不出来,这次换成搜字体一下子就找到原因了:一个 Alacritty 的 issue 里面和我有同样的问题,不过他是 tmux 的分割线接不上头,都是 Monaco 字体。

为什么只有 Monaco 接不上头呢?原来在字符界面下画这些竖线的字符和平时用 Shift+\ 输入的字符并不是一个,这类字符叫做 Box-drawing Character,主要的范围是 U+2500U+257F,这些字符用于在终端里绘制方框或者其它形状,所以应该是没有 padding 的,才能接上头,而 Monaco 这里有问题,它给这些字符加上了 padding,导致接不上。我尝试着给 Emacs 的字体换成 Source Code Pro,竖线立刻就连上了。

怎么解决?换字体?不可能的,我是 Monaco 的狂粉,看惯了 Monaco 再看别的字体都觉得傻了吧唧的。如果不是因为它好看我才不会忍受它这么多缺点(没有内置粗体,虽然是等宽字体内部的连字表却和非等宽字体一样,有版权不能二次分发)。解决方法其实比较简单,把 U+2500U+257F 的字符换成正常字体里的就可以了。简单的解决办法是用 FontForge 的同个实例打开 Monaco 和另一款字体(我选择了 Menlo,Menlo 是苹果用来替代 Monaco 做内置默认等宽字体的,应该会比较接近),然后选择 Monaco 的这个范围清空,然后选择 Menlo 的复制粘贴过来。不过我没在 FontForge 里面找到连续区间选择的办法,上网搜了一下说可以用它的脚本 API 选,办法是打开 File 菜单里面的 Execute Script,执行 fontforge.activeFont().selection.select(("ranges", None), 0x2500, 0x257F)。如果你不知道怎么把 Menlo 里面的一段字形复制粘贴到 Monaco 里面,你也可以在 Menlo 里执行这段脚本,然后反选,全部清空,然后把这部分生成一个字体,再去 Monaco 里面选 Elements 菜单里面的 Merge Fonts。

或者你也可以看看这个 叫 Menloco 的项目,是我偶然间搜索到的有同样问题的用户的解决方案,这个项目包含更多的细微 tweak 脚本,帮你利用 Monaco 和 Menlo 合并出一个 Box-drawing Character 能完美接头的字体。不过有几个地方需要注意,一个是这个项目的作者应该是 macOS 的用户,如果你不是 macOS,需要自己想办法搞到 Menlo.ttf 和 Monaco.ttf,简单的办法是找个用 Mac 的朋友让他发给你,不过有可能你得到的是 Menlo.ttc,需要用 FontForge 打开选择 Regular 字重,然后导出成单个的 ttf。你还需要修改 utils/find-font.sh,这个脚本的 font_paths 只包含 macOS 放置字体的目录,你得加上你自己放这两个字体的目录。以及这个项目默认生成的字体名(不是文件名)叫 Menloco,如果你不想修改已有的写着 Monaco 的配置文件的话,就把 merge 这一项下面的 --font-name=$(RESULT) 改成 --font-name=$(INTO) 就好了。

生成一个没问题的字体之后你还可以像我一样用 FontForge 做一些修改,比如说我发现 Menlo 有很多 Monaco 没有的字符,于是我直接把 Menlo merge 进了生成的字体里。并且我之前提到过 Monaco 作为一个等宽字体,内置的连字表竟然是非等宽的,不是像 Fira Code 那种把不同的编程符号连字起来同时保持等宽的连字,而是像普通无衬线一样把 fi 一类的字符连起来变成单个字符宽度。我被坑得最狠的一次就是 review 同事的 patch,我问他这里是不是少了个空格,他说在他那看没问题,最后我发现是 Monaco 连字了!虽然你可以通过配置 fontconfig 关闭连字,但是 Firefox 是不吃这个配置的,而你也不可能给每个网页的代码块都加上关闭连字的 CSS。所以我直接在 FontForge 里面干掉了连字表,具体方法就是打开 Element 菜单下面的 Font Info,点击左侧的 Lookup,选中带 liga 的项 delete 之后导出字体即可。

还有一个比较古怪的 Emacs 问题,Emacs 设置字体和其它程序不太一样,可以先设置一个默认字体然后针对不同的字符集设置不同的字体,一般要为中文单独设置字体才能得到合适的效果,就像下面这样:

(set-face-attribute 'default nil
                    :family "Monaco"
                    ;; :slant 'normal
                    :width 'normal
                    :weight 'normal
                    ;; 1 height is 1/10 pt.
                    :height 140)

(dolist (charset '(kana han symbol cjk-misc bopomofo))
  (set-fontset-font t charset (font-spec :family "Noto Sans Mono CJK SC"
                                         ;; :slant 'normal
                                         :width 'normal
                                         :weight 'normal)))

首先要注意 height 的单位是 1/10,所以你想要的字号需要乘 10 才行。

然后你会发现明明你只设置了一个字号,可是中文和英文字体却不是等高的!也就是说如果你在本来都是英文的一行里面输入一个中文字,那这行的高度就会突然跳一下变高,非常烦人,也许是这两个字体在同样的字号的时候尺寸并不完全一致,但是明明其它程序都能正常处理,为什么这里这么怪!

你可能会想要对中文那段单独设置 size 缩小一点,但是这样不行,你用 C-x C-= 放大字体的时候中文字体就会固定大小不跟着你变了。正确的解决方法是加入下面一句:

(setq face-font-rescale-alist '(("Noto Sans Mono CJK SC" . 0.85)))

这里你可以对任意的字体指定缩放参数,不会影响按键放大缩小。我尝试了一下 0.85 比较合适,虽然可能这样汉字看起来会稍微小一点,但是 0.9 就太高了仍然会跳。或许你会问那这样中文字宽不是英文字宽两倍了?那没有办法,Monaco 本身就属于一个比较宽的字体,那些满足英文宽度是高度一半的字体都比较瘦长,我个人是不喜欢这样的风格的,所以对于我来说等高就行了,宽度我不太在乎。

更新(2022-02-25T19:01:31):我最近研究了一下,发现原来字号并不等于行高。虽然字体有一个 em size 作为基础的方块大小,但是设计师经常指定一些奇怪的 ascender 和 descender 值让字体的高度超出 em size……我不太清楚 Atom 或者说 Chromium 是怎么进行中英文混排的,不过 Emacs 排版时候是对齐 baseline,然后 ascender 和 descender 完全一致才能保证行高不会在切换字体时候变化。但是这实在是太难了,大部分中文字体和英文字体都对不上,特别是 Noto Sans CJK 系列,不知道为什么比其它的高特别多。一个简单的解决办法是使用等距更纱黑体,里面混合的英文等宽字体 Iosevka 和中文的思源黑体拥有一致的 ascender 和 descender,但我实在是不喜欢 Iosevka。并且不知道为什么,Noto Sans CJK 和等距更纱黑体里面的内置的思源黑体应该是同一种字体,Noto Sans CJK 就要比他高很多。我还尝试了修改 Monaco 的 ascender 和 descender,不过这部分非常复杂,涉及到好几个不同的值,而且不同字体比例尺也不一样。最关键的是修改之后相当于把字体拉高了,于是 box-drawing character 又接不上了……上面那个修改缩放参数其实也不是无级缩放,实际上是乘字号之后取整然后再去找对应大小的字符,所以其实就是找小几号的 Noto Sans CJK。具体的可以在 Emacs 里面 M-x describe-font 查看详情。

更新(2022-02-26T09:51:25):补充一下,浏览器的混排应该和 Emacs 是一样的,我刚才尝试了一下,我的博客行高固定是因为我给 <pre> 设置了 line-height: 1.5,这个大小超过了 Noto Sans CJK 的行高,如果删掉这一行,你就会发现含有 Noto Sans CJK 的行比只有 Monaco 的行要高很多。不过 Emacs 没办法像浏览器一样指定一个最小行高,只能是它自己根据这一行的字符计算行高,所以没什么比较好的解决办法。

搞定这些之后,至少 Emacs 里面看起来比较顺眼了。

正常的样子

by Alynx Zhou (alynx.zhou@gmail.com) at February 26, 2022 01:51 AM

February 17, 2022

Lainme

解决Snapcraft的方框字问题

从snap安装的软件经常会出现文字显示为方框的情况,目前发现一个比较有效的解决方式如下:

先执行

snap run --shell [软件名]

然后执行

fc-cache -r

运行结束后退出即可。

by lainme (lainme@undisclosed.example.com) at February 17, 2022 02:57 PM

February 15, 2022

Alynx Zhou

不写文章的博客生成速度最快

春节假期虽然是假期,但是在家的时间里我基本没闲着,毕竟按照传统(指 Ken Thompson 在假期里写出了 UNIX 的第一个雏形的传统,不过维基百科上并没有详述是不是这样,我也懒得考证),假期是造轮子的好机会。所以我就胡乱捣鼓一通看看能不能让我的博客生成器更圆一点。

重新理解 await

第一个简单的修改就是把启动时加载文件的部分并行化,其实我也没有非要尽可能并行,性能也不是我第一位的追求。

我一开始写代码的时候知道有 await 之后就开始在各种地方给 Promise 用 await,随便你怎么说都行,后来我也才意识到,给所有的 Promise 都加上 await 不就是相当于每一个 Promise 都写在上一个的 then 里面,那不还是顺序执行吗?写多了以后对异步有理解了我才想起来这里完全可以用 Promise.all(),反正加载插件/脚本/模板/语言是不太有先后依赖顺序的(非要说的话,插件和脚本里可能包含模板引擎,所以可能下一步是给前两个和后两个分开顺序加载)。

程序解决不了的事情就让人来做

随便你怎么说,我一直觉得有些程序很难理解的事情而人类很好理解的就该让人类来做,而不是费很大力气得到不好的效果。一个多少接近这一条的问题就是主题文件的依赖问题——很多生成器比如 Hexo 在以 server 模式运行的时候都有 watch 功能,可以监测文件变化然后实时重新生成,用户只要一刷新就可以看到最新的更改。直接看上去不难,写 Node 的谁还不知道 chokidar 了。但是问题就在于,普通用户写文章不太需要一边看渲染一边写(你不会连 Markdown 这么简单的格式都不能脑补吧?我鄙视 Typora),反而是主题作者经常需要看到自己刚编写的样式是什么样子,而这是一个很复杂的地方,CSS 预处理器和 HTML 模板引擎通常都有 import/include 一类的语法,让你不用写很多重复的片段,而博客生成器并不能理解每一种预处理器和模板引擎的语法。假如你在 A 模板里 include 了 B,然后修改了 B,此时你刷新一个用了 A 模板的页面,你是看不到页面更新的,那在最需要这个功能的时候这个功能变成了废物。

有一个简单的解决方案是不要预编译你的模板,而是每次需要渲染页面的时候重新读取模板文件编译出一个函数来。不过这对于生成器而言不太优雅,有些模板引擎比如 nunjucks 可能有内建的 cache 支持,但另一些可能是没有的。再说你来来回回读这么多次磁盘,那干嘛不直接把渲染页面的工作也挪到用户浏览器(指前端)里呢?还要博客生成器做什么。

我一开始的打算是既然生成器自己不好理解依赖语法,那就像处理博客文章一样,让主题作者给文件加上 front matter 记录这个文件的依赖。不过问题又来了,对于直接使用的模板(指博客生成器读取并编译它们)可以在读取时去掉 front matter,但是假如这个模板 include 的模板也带有 front matter,此时实际上是模板引擎(比如 nunjucks)自己去磁盘上读那个文件,它不会去掉 front matter。而给每一种模板引擎都重写文件加载器显然是不现实的,所以最后的方案就是要求主题作者提供一个 file-dependencies.yaml 的文件,里面记录每个文件的依赖关系,大概像是这样:

layouts:
  includes/layout.njk:
    - includes/footer.njk
    - includes/header.njk
    - includes/sidebar.njk

第一层是需要 watch 的目录名,因为不是所有的目录都需要实时刷新,显然这种耗时的操作越少越好,第二层是依赖其他文件的文件,第三层则是一个被它依赖的文件的列表。程序里面我写了一个 Watcher 类,启动时会读取这个文件,然后将它翻转过来:以被其它文件依赖的文件作为 key,而依赖这个文件的文件成为 value 列表。因为我们总是检测到被依赖的文件变化之后才开始统计受到它影响的文件。然后就是一个递归收集受到影响的文件的函数,因为文件依赖并不是只有一层的。收集之后分别给这些文件也加入到需要更新的列表里面。

写完这个功能之后我最大的感叹就是:要是我写主题的时候能有这种功能,我应该能节省不少检查的时间吧……

后面闲着又给这个做了点修改,一个是支持用 glob pattern 作为被依赖的文件,虽然我个人觉得还是把需要的文件都列出来算了,但是想必对于处在开发调试期的主题来说很有用,作者可能不一定想得起来及时更新这个文件。另一个是考虑到使用 glob 之后很容易在无意间搞出循环依赖,又写了在递归里打破循环依赖的功能,我通常不擅长处理递归的逻辑,但我脑子灵光一闪想起来可以给函数最后一个参数设置成一个记录已经处理过哪些文件的 Set,然后递归的时候作为参数传进去,然后就这么成了。

现在如果开启 server 模式,各种文件以及语言文件模板文件,甚至包括这个依赖描述文件自己都是实时更新的了,只有主题和站点的两个关键配置文件需要重启之后才能重新加载。

削减依赖的路上没有最少只有更少

事实上我在添加依赖的时候已经非常克制了,我尽量只使用那些我不得不用的 npm 包,有些功能能自己实现的我都自己实现了。但是总有还能去掉的依赖。我首先干掉了 Stylus,理由很哭笑不得,它好像不是那么流行了,我都找不到合适的支持它的 Emacs 插件。我本来想换成 Less 或者 SCSS,但是前者语法和 Stylus 差得有点多,后者有好几种语言的实现,本来我看 GNOME 都在用 C 写的 libsass,我觉得挺好,也打算用这个,结果发现它教程里挂着说“不推荐使用 @import 而推荐 @use 但目前只有 dart 版本的实现支持了后者”,让我感到说不出来的难受,我不太喜欢这种奇怪的新语言,而且搞这么多实现,还整出参差不齐的语法,实在是麻烦,虽然 dart 可以编译到 JavaScript,我还是放弃了。其实我自己写 Stylus 的时候不是放飞自我型的,而是尽可能贴近 CSS 型的,所以干脆把我的样式都改成了原生 CSS 了,顺手就干掉了一个依赖,如果有需要的人还是装插件吧。

再之后就是我一直使用 glob 来匹配文件,但是它还依赖一些别的包,我在想能不能用依赖更少的包替换它。glob 使用 minimatch 分析 glob pattern,但是比如 chokidar 使用的是另一个替代品叫 picomatch。我找到一个叫 fdir 的库,虽然接口用起来怪怪的,但是如果你用它做 glob,只需要它和 picomatch,没有引入别的依赖,于是我就试了一下。但是不得不感叹作者能不能写点阳间的代码,这个库默认忽略软链接文件,这显然是不能用的,但是假如你设置解析链接文件,它倒好,就算我要的是相对路径,它还是直接解析并拼接了绝对路径——我只是想让你把链接文件当普通文件返回一下!就在我打算放弃并用回阳间的 glob 包的时候,我发现 chokidar 自己依赖的 readdirp 库也可以配合 picomatch 使用。但是如果你直接把 glob pattern 丢给它让它自己调用 picomatch 编译的话,又有一些阴间的限制,索性可以给它传自定义的过滤器函数,正好我还需要支持一些别的功能,就自己编译 glob 传给它了。

实际上我现在只有 10 个左右的直接依赖,全部的运行依赖大概是 36 个,在 Node.js 编写的程序里面应该算是相当克制的了,考虑到这里面大部分都是 nunjucks 的间接依赖,我觉得我做得相当不错。

对于其它我编写的简单的 npm 包我还是比较追求 0 依赖,只有 0 依赖的包才是比较可控的,如果一个 npm 包在 npm 上显示有 1 个依赖,那你就不能确定这一个依赖有多少间接依赖,而不更新的间接依赖会引入多少安全问题你也无法得知。我在给 Hikaru 选依赖的时候一般也会倾向于 0 依赖的包(这就是为什么选择 commander.js 而不是 yargs 的原因)或者是一些虽然有依赖,但是递归翻几次就是 0 依赖的包(chokidar 属于这类,而且我还给 nunjucks 提过一个 PR 使用 commander.js 代替了 yargs),而不会选那些依赖摞依赖无穷无尽的包。

这里不得不吐槽一下 jsdoc、mocha 这些大型的开发用的工具库,依赖实在是太混乱了,甚至 jsdoc 似乎同时依赖 marked 和 markdown-it……开发者写新功能的时候能不能长点心啊。还有我之前用 react-scripts 的时候,用旧版提示依赖里面有的版本有安全问题,升级到新版依赖算了一大堆,甚至出现了不同版本的间接依赖,而且仍然是有问题的版本,还是希望开发者对自己的依赖都关心关心,及时更新依赖版本,不要锁死一个版本懒得改就觉得万事大吉了。

如果你对削减 npm 包的依赖感兴趣,强烈建议你读一下 chokidar 开发者写的这篇文章

worker_threads 是多线程,但和我想的不太一样……

我:你懂 Node 吗?为什么我多线程比单线程慢? 铁道迷:Node 就是单线程拖拉机啊。 我:你不懂,88。

凡是跟你说“Node.js 只有单线程”的人,都可以直接和他说“你不懂 Node.js”了。Node 要解决的就是非阻塞 IO 的问题,而非阻塞 IO 肯定不是多线程能解决的。至少对于内部的 fs 来说,调用异步函数的时候是 libuv 从线程池里面拉出一个线程去解决任务,而 JavaScript 本身的线程不会被阻塞。如果你还想了解“有哪些代码会阻塞主线程而哪些代码不会”,可以看 Node.js 官网的这篇文章

说实话,对于用户自己写的 JavaScript 代码段,似乎是没什么好办法把它丢到主线程之外的线程上去执行的……请务必注意“回调”,“异步”和“非阻塞”的区别——不是所有回调都是异步的,比如我传了一个回调函数它也可能是同步的,而就算你把一段耗时的同步代码套上 setImmediate 和 Promise,也不意味着它就不会阻塞主线程了……它执行的时候还是在主线程执行,只是你把它延迟了,推到了 event loop 空闲的时候去运行——但轮到它运行起来还是阻塞住了主线程。(说出来不怕笑话,我也是写得多了最近才认识到这些区别(乐),如果我写的不对还希望大家指正。)

起因是我读了 Sukka 的这篇文章,介绍了 Node 新加的 worker_threads 模块,是允许你开启子线程运行代码的。正好我想到我的博客生成器里有一段分析每篇文章内容并修饰里面的图片和链接的代码,要是能起一个线程池,把每个文章分配给不同的线程去处理,那多是一件美事啊!

我发现这个 worker_threads 并不是很好用,它并不像 pthread,直接运行一个函数,而是要求你必须传一个 JavaScript 脚本的路径进去,这就给代码编写搞出了很大的麻烦。我做的修改在另一个 worker 分支里面,可以访问 https://github.com/AlynxZhou/hikaru/commits/worker 看到。不过一个尴尬的事情是写好以后我跑了几次,发现多线程还没有我之前单线程运行快……而且很反直觉的是线程越多越慢,在我 12 核心的处理器上四个线程是跑在主线程之外最快的,比什么 2、6、8、12 都快。

说实话,我也没有搞清楚到底是怎么回事,可以确定任务确实是分发给了不同的线程,但是为什么会这么慢呢?一个可能的原因是在不同的线程之间并不是像 C 一样直接共享内存空间,传递参数的时候 Node 是复制之后传递的,虽然我觉得我传递的数据量不至于让复制造成瓶颈吧,但这是我能想到唯一的原因了。而且阅读例子的时候也发现,好像推荐的用法是比如做 http server,每一个线程都是启动之后一直监听,而不像我这样是不断的分配大量的小任务。总之我很需要一个像 pthread 那样简单的给我执行一段代码的线程,而 worker_threads 好像倾向长时间运行一个单独文件……强扭的瓜不甜,我还是放弃吧。

然后说回到性能这件事上,我倒不是觉得生成器现在的性能难以忍受,否则我就该用 Hugo,相反我实在是觉得现在的生成速度出乎意料的棒了,而且又不像 Hugo 只支持 go template,Node 有很多种模板引擎可以选(我就是不喜欢 go 你来打我呀)。完整生成一遍我的博客大概耗时 800 毫秒,而且我也没有把渲染啦解析啦之类的功能用多线程解决,并且我的生成器没有缓存,每次都是从头生成的。没有缓存这点其实和之前 watch 文件的功能很类似,不够可靠还不如不要算了。在我以前用 Hexo 的时候它是有缓存的,但是这个缓存经常导致奇怪的结果,比如该更新的页面没有伴随文章一起更新之类的,我也不是很理解到底怎么回事(特别是考虑到 Hexo 里面还有一个奇怪的 JSON 数据库 warehouse),所以每次我都是 clean 之后再从头生成,那我自己实现的时候自然就砍掉这种不靠谱的功能了。

当然我问了琪神 Hugo 生成他的博客耗时多少,似乎只需要 200 毫秒的样子,不过除了 go 是编译型语言而且 Hugo 肯定用了协程渲染以外,他只有 6 篇文章而我有 83 篇文章也是一个很重要的因素!他甚至连分页都不需要生成!

所以如果你真的只追求速度不追求别的,什么语言都是浮云,总结出来就一句话:不写文章的博客生成速度最快!

by Alynx Zhou (alynx.zhou@gmail.com) at February 15, 2022 10:33 AM

February 11, 2022

Alynx Zhou

StackHarbor 的 2021 尾记

今年一直拖着没写总结,不是因为懒,主要是因为我总觉得好像距离去年写总结也没太久,我还能回忆起来去年写总结时候是什么样……也说不上是因为我记忆力不太好还是因为这一年实在没什么能让我记住的事情。

2021 年工作没什么变化,不过我换了个好一点的住处,虽然仍然是旧楼但是里面是新装修过的,至少让人待着的时候感到舒适,而且还可以吸室友的猫,虽然小猫也经常搞破坏,不过猫做什么都可以原谅。

看了去年的总结,才发现我是 2021 年把 FlipClock 改成 Meson 管理编译的,于是想起来我这一年学了 Meson 怎么用,因为 CMake 实在是让我烧脑筋。我今年还终于让 scrcpy 能够模拟 USB 键盘,这样使用 scrcpy 的时候也能用数字键选词了,多少算是在一个大项目里面做了一个大贡献。

然后其他能记得住的就是最近折腾的一些东西了,春节假期没写博客也是因为忙着写代码。写了一个简单的网页小游戏,基本是平面版神庙逃亡,感觉还是挺有成就感的。然后给之前的弹钢琴页面改成了原生 JS 实现,放弃 React 的原因是依赖太多了,而且不是每个软件包都能及时更新自己的依赖。比如你用一个老版本的 create-react-app 会带来许多有漏洞的依赖,然后你更新到最新版因为依赖变了,有些库又依赖了另一个有漏洞的版本。我觉得为了简单的 UI 和数据绑定这个代价是不值得的,所以我就重写了一个。然后给我的博客生成器添加了文件依赖的支持,很久很久以前当我刚开始用静态博客生成器的时候就在头疼这个,博客生成器理解不了你的模板引擎或者预处理器的导入和扩展语句,于是没办法在一个文件更新的时候更新所有包含它的依赖,导致主题开发要不停的关掉生成器再开。一开始我是想给这些文件都加上 front matter,但是我发现虽然我自己加载的时候可以去掉 front matter,模板引擎处理导入语句的时候是他自己读文件所以没办法去掉 front matter,我不想给每个模板引擎都写自定义的 loader,所以最后改成用一个单独的文件记录依赖了。最后的结果就是单独抽出了一个 Watcher 类,可以查表查出所有需要更新的文件然后调回调,效果还挺不错的。以及本来我想清理一下博客的样式然后顺便换个 CSS 预处理器,因为感觉 Stylus 不是特别火了,但是考察了一下,less 的语法和 Stylus 差得有点多,而 scss 虽然使用广泛,而且有纯 C 语言写的实现,但是它有好多个实现,并且官网上说只有 Dart 语言写的版本才不过时,其它的都只实现了过时的 @import 而用户应该用最新的 @use,总之搞得这么麻烦看起来就不想用。虽然 Dart 能编译成纯 JS,我还是觉得没兴趣。于是最后我把我的样式都改成了原生 CSS,然后去掉了生成器内置的 Stylus 支持。

显示器从去年的一个换成了两个,因为我是一个经常最大化窗口的人,切工作区虽然快,但是比如在浏览器和代码编辑器之间切换的时候还是会打断思路,有了双屏就没这个烦恼。一开始我想的是双屏更方便一边全屏游戏一边干别的事情,不过好像也就那样,因为从全屏切出来到另一个屏幕也不是很顺畅。不过买双屏的时候很头疼的就是因为不是一个批次,两个显示器的色温差得不是一点,最终是换了一台然后用校色仪调了好久,虽然不是 100% 一致,但是至少差不多了。

说到这个我想说,感觉幸运和倒霉是伴随而来的,不太可能一直幸运,经常是发生一些开心的事之后又发生一些不开心的事,比如去年买了新相机,经常没事拍点照片什么的,结果昨天晚上我突然发现不开镜头盖的时候 CMOS 有个红点……我觉得我基本可以排除激光损伤,应该就是坏点,但是我用机内的像素映射却解决不掉。感觉最后还得跑一趟维修站,而我在老家,一时半会不会回北京,这边也没有维修站,总之麻烦的事情就会让我心累。买了一张升降桌,不过这个最便宜的款式不太靠谱,现在升降功能不太正常,于是只是变成了一台大桌子。不过椅子坏了之后换了个比较便宜的工学椅倒确实令我满意。

今年没有换手机,主要是把这个钱投资到镜头或者灯上让我更高兴一点,手机厂商出的手机真是越来越烂了。不过把备用的 iPhone 8 换成了 iPhone XR,在二手频道看到一个成色很好,而且是红色款的,我早就觉得 iPhone 8 屏幕太小了。

动漫也想不起来看过什么,今年也许只看了 EVA 最后一部剧场版和 love live superstar,反正看 EVA 就是大家都想看看他怎么收尾而已,总体来说还是不错,特别是看了纪录片之后。然后 love live 我以前没看过,我的评价就是挺好听的。然后我感觉在电影院看到 Fate/Stay Night HF 第三季的可能性应该是没了,特别是考虑到今年院线上的国外电影的比例……还能说什么呢懂得都懂哈哈。

想不起来看过多少书,总之前段时间有天晚上睡不着,一口气把挪威的森林看完了,我早就买了这本书,但是以前都是打开看几页就看不下去了,这次一口气看完感觉还挺好的。

今年的另一个好消息是 NVIDIA 驱动终于补全了 Wayland 支持,感谢所有在这个过程中努力的人,比如 NVIDIA 那个一直在处理相关事情的用蜗牛做头像的老哥,事情总要是靠人推进的嘛。不过回家之后莫名发现我的 Atom 在 XWayland 下面会疯狂地闪回,明明我自己住处的电脑也是一样的配置就没问题。然后这时候我发现 Emacs 的纯 GTK 分支已经合并进了主线,也就是说 Emacs 现在终于是纯 Wayland 程序了,我立刻搞了一个最新版本,然后捡起来以前的配置文件,又仔细研究仔细鼓捣,目前多少是堪用了,虽然还有不少地方不像 Atom 那么习惯,主要还是我懒得仔细研究,反正凑合用,我是真的怀念 Atom 上的一些功能,比如注释反注释代码块、移动代码块、方便的查找替换还有现代化的界面……Emacs 的界面元素感觉还是基于文本的,然后包括粗体字渲染感觉也不太光滑,比如 indent guide 或者 80 column ruler 这种东西就应该直接在界面上划线嘛,不要用字符做。不过我在用 Atom 的时候也怀念 Emacs 的一些功能比如纯键盘分屏和 C-SPC Mark Region。

今年的游戏时间基本都拿来打 Dota 了,虽然看 TI 看得很难受,但是和朋友一起玩游戏还是挺快乐的,这也属于幸运和倒霉的周期了吧。塞尔达什么的,等我想起来慢慢玩……

跨年的时候还是和去年一样朋友聚餐了,我还在 B 站上传了 vlog,和蓝猫铁道迷一起吃寿喜锅很开心。今年写代码没少让琪神当小黄鸭,谢谢琪神。

总之先写这些,如果我又想起什么,我后面再慢慢加上吧,最近冻着了嗓子疼,希望倒霉的周期赶紧过去,过段时间还得找找机会去修相机呢……

by Alynx Zhou (alynx.zhou@gmail.com) at February 11, 2022 01:18 AM

February 03, 2022

中文社区新闻

调试符号(debug)包和 debuginfod

我们很高兴地宣布 Arch Linux 将有调试符号包了。
我们的 debuginfod 实例将提供调试符号信息和源码列表,这些可以被调试器比如 gdb 和 delve 利用。
https://debuginfod.archlinux.org/
一些由赞助商提供服务器的镜像站已经开始提供 debug 软件源的镜像,同时我们正在商讨新的镜像站要求

目前并不是所有包都提供调试符号包,这是我们正在进行的工作。
更多信息请参阅 Debuginfod 维基页,以及我们近期刚更新的 Debugging/Getting traces 维基页。

by farseerfc at February 03, 2022 04:33 AM

February 02, 2022

百合仙子

Wayfire 迁移进展(四):不那么 high 的 DPI

本文来自依云's Blog,转载请注明。

使用24寸4k屏幕作为主屏的时候很简单,设置 scale 为 2 就好了。但是,当 2 嫌太大、1 嫌太小的时候,问题就来了。比如我希望使用 120dpi,把 scale 设置为 1.25 可好?

scale=1.25 text

而这才是理想的效果:

120dpi text

看不出来差别?放大八倍,你看差别多明显:

8x compare

正常 120dpi 渲染出来的文字边缘清晰犀利,次像素平滑左红右蓝。再看看 scale=1.25 的文字,线条经常糊掉,次像素平滑效果几乎完全被抹掉。实际看上去的效果就是跟透明麿沙玻璃看屏幕似的,线条边缘总是有点糊糊的感觉,1080p 的屏幕被降级成了 720p 似的。

之所以出现这样的情况,是因为 Wayland 只支持整数倍缩放。因为,Wayland 混成器不能告诉客户端你得把窗口给画成 1.25 倍的,而客户端也无法告诉混成器我这个图像画的是 1.25 倍。所以,混成器只好告诉客户端你给我画个 2 倍的图像吧。混成器拿到图像之后再缩小 0.625 倍,自然有些逻辑像素就不能对应到单个的物理像素上去了。

所以,我还是设置 scale=1,不要混成器帮我去缩放。我自己通过另外的办法告诉客户端把字写大点儿。图标之类的就顾不上啦,反而大点小点都还能看。比如我要 1.25 倍大小的文字,就这样做:

  • GTK 3:在 dconf 里设置org.gnome.desktop.interface.text-scaling-factor=1.25就好了。最开始的截图就是 dconf-editor 里这一项配置。
  • Qt:设置环境变量 QT_WAYLAND_FORCE_DPI=120
  • Telegram:除了上边这个环境变量外,额外地在它自己的设置里设置 150% 的缩放(Telegram 的字偏小所以要设置得大一些)。设置环境变量是为了 fcitx5。
  • waybar:config 文件中设置 heightstyle.css 中设置 font-size
  • Xwayland:和 X11 下的 HiDPI 设置差不多的。比如 GTK 2 设置 Xresources Xft.dpi: 120 就好了。

我遇到的差不多就这些了。没办法,Linux 就是这么乱 QAQ。不过虽然 Wayland 协议不支持,好歹还有绕过的办法。

by 依云 at February 02, 2022 09:23 AM

January 25, 2022

ヨイツの賢狼ホロ

咱和()的 2021 年

有些事情在慢慢变化的嘛,例如咱逐渐开始偷懒到只在 Matters 发表文章这件事。 以及换服务器玩脱了把咱自己的 Mastodon 搞爆炸了什么的……

可以去那里看看咱这一年写了些什么,和发了什么牢骚 。(笑)

以及人老了就喜欢偷懒啦,所以后面大概主要会用那边的 WordPress 来写文章了。

在这里,名字还是一样的。

至于这里嘛,虽然后面可能不会再有大的更新了,不过咱应该还会尽力留着。 万一哪天咱又有心情开始折腾了呢……

咱也谢谢和咱一起路过这六年的大伙儿了。

by ホロ at January 25, 2022 04:00 PM

January 22, 2022

中文社区新闻

linux-firmware 20220119.0c6a7b3-2 需要内核 >=5.3 以及做了拆包

linux-firmware 从 20220119.0c6a7b3-2 开始实现了内核固件压缩。 Linux 内核从 5.3 开始支持加载 xz 压缩过的固件。这需要启用
CONFIG_FW_LOADER_COMPRESS 内核编译选项。所有官方 Arch Linux 内核早已开启了这一选项[1]。

并且 linux-firmware 包已经被拆分成多个小包,进一步减少磁盘占用。拆包将一些很少使用的硬件上较大的固件文件分到了独立的包中。受影响的硬件包括: Mellanox Spectrum 交换机, Marvell 设备, Qualcomm 芯片集, Cavium LiquidIO 服务器适配器, QLogic 设备, Broadcom NetXtreme II 10Gb 以太网适配器。
如果需要的话请确保安装这些附加固件包。 [2]

[1] FS#72899
[2] FS#72559 + svn commit

by farseerfc at January 22, 2022 12:54 AM

December 31, 2021

Leo Shen

2021 Recap: Seek

And here goes 2021. Here's a snippet. What I Wrote In 2021 I wrote 3 English articles and 2 Chinese articles. I actually had a lot of blog ideas during this year, but the vast majority of them failed to become a full blog post. It's so hard to write something that is interesting and unique at the same time. Maybe I should lower my standard a little bit.

December 31, 2021 11:59 AM

December 27, 2021

frantic1048

Pop Up Parade - 真红(Rozen Maiden)

Shinku

看到这款真红的消息时十分激动,竟然能看到早年看的作品里喜欢的角色出手办,宣传图看起来也没啥问题,于是就订了。

拍照的时候想着搞个暗色的环境更搭一点,换上了纯黑色的背景纸,但是没想到反光太强了,拍出来变成了灰色,Darktable 里稍微调了一下也是深灰色。然后尝试灯亮度下来一点,结果噪点过多又拍了第二波。折腾一番之后发现深灰色也挺好的。

头一次遇到不是用纸质而是全透明的包装,还蛮好看的。

Shinku

本体没有需要组装的部件,直接放底座上就好了。巨长的双马尾保护得很好,没有出现悲剧,可喜可贺。

Shinku Shinku Shinku Shinku Shinku Shinku Shinku Shinku Shinku Shinku

Shinku Shinku Shinku Shinku Shinku

比较奇妙的是一些角度看过去,真红的微笑就消失了,特别是稍微偏低一点的角度。

Shinku

角度抬高,微笑又回来了。相机还是抬高一点拍好。

Shinku Shinku Shinku Shinku Shinku

极限距离!

Shinku

December 27, 2021 05:07 PM

中文社区新闻

libxml2>=2.9.12-6 更新需要手动干预

libxml2 包在版本 2.9.12-6 之前缺失了预编译好的 python 模块。这个问题已经在 2.9.12-6 中修复,所以更新时需要覆盖未被跟踪到的 pyc 文件。如果你在升级时遇到如下报错:

libxml2: /usr/lib/python3.10/site-packages/__pycache__/drv_libxml2.cpython-310.opt-1.pyc exists in filesystem
libxml2: /usr/lib/python3.10/site-packages/__pycache__/drv_libxml2.cpython-310.pyc exists in filesystem
libxml2: /usr/lib/python3.10/site-packages/__pycache__/libxml2.cpython-310.opt-1.pyc exists in filesystem
libxml2: /usr/lib/python3.10/site-packages/__pycache__/libxml2.cpython-310.pyc exists in filesystem

更新时请使用命令:

pacman -Syu --overwrite /usr/lib/python3.10/site-packages/__pycache__/\*

完成升级。

by farseerfc at December 27, 2021 08:18 AM

December 06, 2021

frantic1048

F:NEX - 伊蕾娜

Elaina

有一位乘着扫帚飞在空中,头戴巨大三角帽,灰色秀发随风飘逸,令某位路过的普通点兔爱好者都被吸引住目光,在智乃生日的日子掏出相机疯狂拍照的魔女,究竟是谁呢?

没错,就是她!

首先来个包装盒。

Elaina

非常重要的呆毛,可惜和帽子不能兼得。

Elaina

没有帽子的情况下,手势显得比较奇妙,像是在和另一个人比心,或是在说一张照片要卖一百亿金币的事情。(゚ ∀。)

Elaina Elaina Elaina Elaina

戴上帽子手势就科学了!

Elaina Elaina Elaina Elaina Elaina Elaina Elaina

Elaina Elaina Elaina Elaina

Elaina Elaina Elaina

December 06, 2021 06:23 PM

December 04, 2021

百合仙子

Wayfire 迁移进展(三):taskmaid, waybar 以及 mako 等

本文来自依云's Blog,转载请注明。

我又来更新我的 Wayfire 迁移进展啦~

我写了一个 taskmaid 工具,使用 wlr foreign toplevel management 扩展来提供窗口管理相关功能。程序自己作为 daemon 随 wayfire 启动运行,通过 D-Bus 提供接口供别的程序使用。你问我为什么不直接每个需要的程序直接使用 Wayland 协议?因为用起来麻烦呀。Wayland 提供信息的方式是一组一组的事件,也并没有高层次的库,处理起来只能一堆回调怼上去,相当不顺手。

它最主要的功能就是在 waybar 上标题当前窗口的标题啦。顺便加上了中键关闭和显示 app-id 的功能。应用程序图标因为没有办法准确匹配(比如火狐 nightly 版本的 app-id 也是 firefox),所以没有做。

其次是获取当前活动窗口所在的显示器接口名称。我使用这个名称来判断我是不是位于 E-ink 屏幕上,并为终端、Vim、skim、mutt 等工具使用专门适配过的亮色主题。这个方案比之前在 X11 下使用当前鼠标坐标来判断要灵活一些。当然更灵活的方案是匹配显示器的名称啦,这个数据 Wayfire 的 Wayland 协议里是有提供的,有需要的话我再做。Wayland 并没有一个协议来获取当前鼠标或者键盘焦点所在的显示器信息,所以我只好用窗口管理协议来跟踪活动的窗口了。

顺便还做了个 lswin 工具,列出打开的窗口信息。工作区太多啦,我又喜欢最大化,有时候会有窗口忘了关。甚至偶尔我还会不小心把窗口最小化了,然后没有办法给恢复回来……如果以后还经常出现这种情况,我再给 taskmaid 加一个恢复最小化窗口的功能好了。

除了使用 taskmaid 显示标题之外,我还加了个显示 AQI 的小脚本。以及之前忘记加网速指示器了,现在也加回来了。不得不说 waybar 比 Awesome 的那个顶栏要好配置得多。不仅不限语言,而且不是非得用定时器,可以在信息变动的时候及时更新信息,没变动就不浪费资源获取重复的信息了。我的 waybar 配置都在这里

我给 Wayfire 发了一个 pull request,添加了最基础的快捷键禁制器的支持,可以在 spicy 等软件中屏蔽 Wayfire 自己的快捷键了,大大方便了我对 Wayfire 和 Sway 的测试。当然也可以用于 VNC 啦。不过并没有像 Sway 那样加选项、支持主动禁用快捷键等功能,短期内我也不太可能会去加这个。原本我是想着在 Wayfire 里再跑一个 Wayfire 或者 Sway 啥的,但考虑到配置方面的问题,还是拿虚拟机隔离了比较好。

最新的 Wayfire 版本已经支持切换到之前的工作区啦~

桌面通知程序 mako,之前遇到两个问题。一是通知经常是糊的,没有适配 HiDPI 屏幕,而鼠标指针则一直都是又糊又大。我给修了,虽然我其实有点不知道我是怎么修好的……总之是整理了一下代码,为了减少调试日志而减少了与 Wayland 混成器的通讯,也变得更高效了。会记住上次显示使用的缩放倍率,所以不会像之前那样一出来是糊的、下一刻才会变清晰了。在 Wayland 协议里,客户端可以获知有哪些显示器、大小和缩放倍率如何,但是客户端并不能提前知道自己将会显示在哪个显示器上(倒是能够指定显示在哪个显示器上),只有显示出来之后才知道,然后做调整后再更新一下……

另一个问题是,在 Wayfire 下 mako 在全屏时不会显示通知,会被盖住。设置 layer=overlay 之后倒是能在全屏时显示通知了,但是它也会在 swaylock 锁屏界面上显示……后来了解到 mako 有模式这么个特性,我就在锁屏的时候切换到专为锁屏设置的模式下,解锁之后再切回来。反正锁屏是一句命令,自己拿脚本包一下就好了。

最近新的桌面环境稳定下来了,倒是没有再遇到更多的 bug 了。Spicy 会在里边的虚拟机跑特效动画时经常报个「Gdk-Message: Error flushing display: 资源暂时不可用」错误然后退出,我给它用 try_until_success 包了一下倒是问题不大。我也发现了不光是 Wireshark,也有 GTK 程序弹出菜单会显示在错误的位置。

那么就酱~

by 依云 at December 04, 2021 07:52 AM

December 02, 2021

Phoenix Nemo

配置 Fail2ban 保护 Proxmox VE

各种情况下 Proxmox VE 的登陆界面需要暴露在公网的时候,需要使用 fail2ban 来保护它不被暴力破解。

创建 filter

文件 /etc/fail2ban/filter.d/proxmox.conf

1
2
3
[Definition]
failregex = pvedaemon\[.*authentication failure; rhost=<HOST> user=.* msg=.*
ignoreregex =

创建 jail

文件 /etc/fail2ban/jail.d/proxmox.conf

1
2
3
4
5
6
7
8
[proxmox]
enabled = true
port = https,http,8006
filter = proxmox
logpath = /var/log/daemon.log
maxretry = 3
# 1 hour
bantime = 3600

重启 fail2ban

1
~> systemctl restart fail2ban

然后检查是否配置生效

1
2
3
4
~> fail2ban-client status
Status
|- Number of jail: 2
`- Jail list: proxmox, sshd

各种情况下 Proxmox VE 的登陆界面需要暴露在公网的时候,需要使用 fail2ban 来保护它不被暴力破解。

December 02, 2021 01:44 PM

November 21, 2021

ヨイツの賢狼ホロ

怀旧手机游戏不正经研究 - 引子

为啥人(?)老啦就喜欢怀旧咧?

先回忆下咱的手游史

咱最早在手机上玩游戏的时间大概能追溯到十多年前,当时咱住在咱的某一个亲戚家里,作为一个熊孩子(x),偶尔也会摆弄摆弄他的手机。以及到了现在咱还记得那台手机,三星 SGH-i718+。

Samsung SGH-i718

刚才从网上搜索了下,大概就像这个样子。看到那个 Windows 键的话,~~老学校~~玩家应该已经知道了,这部手机搭载的是 Windows Mobile 系统。(以及搜索的时候不经意间发现网上说这是三星第一台国行 Windows Mobile 手机)

Bubble Breaker:Amazon.com:Appstore for Android

虽然内置的游戏只有一个戳泡泡游戏来着(Bubble Breaker?),但是咱不知道怎么发现的(也许是忘了吧)这个手机可以上网,还可以下载 JavaME 游戏来玩,于是接下来的事情就可想而知了……

唉,从五块钱 1M 或者 30M 走过来的表示说多了都是泪啊 😂

以及后来咱也从这位亲戚那里玩到了 Windows Phone 7/8 和 Android 手机,虽然经常是他自己都搞不懂怎么用来请教咱的样子……

后来咱搬去父母家了,虽然他们也忙着做生意经常不着家,于是为了联系方便,就给咱买了一部手机。 Nokia 6700 Slide。

Nokia 6700 slide

虽然那个时候的咱并不知道啥智能手机塞班系统什么的,但是它也能和咱最早玩到的那部手机一样可以安装游戏。

以及它竟然还支持 3G 网络,加上咱当时用的也是联通卡,于是流量和话费迅速消耗…

然后在不知道什么时候惨遭标准结局(丢了)…… 在挨了顿骂几个月以后,家里还是给买了新手机,还是诺基亚。

Nokia C7

就是 Nokia C7 啦,后来才发现咱直接跳过了 S60v5 整个时代。(虽然咱还是能发现有同学买了 S60v5 系统的手机来着,例如堪称街机的 5230 )

Nokia 5230

除了触屏这个显而易见的特性以外,C7 还有当时颇为珍贵的 Wi-Fi 功能。(以及后来才知道当时国内为了推广自有标准 WAPI 强行让国行手机阉割 Wi-Fi 这档事)于是找地方蹭网一度也是咱的活动之一,直到家里装了宽带和无线路由器。

虽然没有数字按键了,玩之前玩过的 Java 游戏有些不方便。但是咱靠它玩到了很多塞班 3 游戏啊,大部分的画面都比那些 Java 游戏好多了。(嗯,咱那时候完全没听说过 N-Gage……)

Asphalt 5 (2009 video game)

例如现在还有更新的狂野飙车(Asphalt)系列,最早是 Gameloft 在 2004 年发表在 NDS 和 N-Gage 的游戏,后来也移植到了 iOS 、Android 等新的手机平台。图片上是咱记忆里在 C7 上玩的 Asphalt 5 。

再后来的某一天呢,咱父母的手机坏了,于是就以再给咱买一台作为交换换走了咱这台 C7。当时 Android 也开始流行起来了,于是鬼使神差间咱买了三星 Galaxy Tab 2 7.0 。

Samsung Galaxy Tab 2 7.0 P3100

没错,一台能打电话的平板!😂 所以咱那个时候怎么想到买它的呢。

于是接下来就是延续到现在的折腾 Android 的故事了, ~~就和咱接下来要聊的话题没太大关系了……~~

至于 iOS 嘛,咱买的第一部 iPhone 是 iPhone SE,于是就和怀旧更没啥关系了。

好啦好啦,虽然老的 iOS 和 Android 游戏也有不少佳作啦,但是现在的新系统上面几乎都运行不了那些老游戏啦。

至于去淘一下老手机平板什么的, ~~经费有点不足……~~

那么接下来呢?

在看了好几个同样爱好怀旧游戏的爱好者的视频以后,咱也被一股不可名状的引力所吸引开始在网上收购一些老手机了。(以及在自己和亲戚那淘一淘货)

于是接下来咱有点兴趣的问题就是,咱想玩 Java ME/Symbian/N-Gage 游戏的话,用哪部手机比较合适?

当然啦,假如自己还有能用的手机的话,那肯定是用现成的最节省成本。于是问题就演变成了如果要淘一淘二手的话哪些型号性价比更高了。

以及有些平台已经有差不多的模拟器了(例如模拟 JavaME 的 J2ME Loader 和模拟 Symbian 的 EKA2L1 ),那么在手机上用模拟器是否也还行呢?假如汝不是特别介意手指搓玻璃的话。

以及带物理键盘的 Android 手机搭配模拟器会不会效果更好来着,例如上一篇文章里那台多亲 F21 Pro。其它的有按键的 Android 手机(像是三星的~~土豪专用~~心系天下系列和黑莓的最后几部键盘 Android 手机)应该也能类比。

~~其实还是没钱买~~

那么是不是还可以扩展到其它的老电玩平台呢……像是 3DS/PSV 或者 PS3/Wii 之类的。

这大概就是长期计划了……说来刚刚打开某软件搜索价格的时候发现咱之前两百块收的 PSTV 已经涨的飞起了,虽然并没有什么关系(x)

于是接下来就是等快递了。 ~~(连鸽子都能说的这么清新脱俗)~~

by Horo at November 21, 2021 04:00 PM

November 20, 2021

百合仙子

Wayfire 迁移进展(二):Xwayland HiDPI 以及 waybar

本文来自依云's Blog,转载请注明。

这几天完成了一个很重要的功能:我让 Xwayland 支持 HiDPI 了!

实际上让 Xwayland 支持 HiDPI 的补丁早就有了,但是我当时尝试的时候补丁并不能很好地应用,我手动修了修,不明不白地应用上之后,并没有能够正常使用。现象是,DPI 是对了,但是窗口大小会不断地缩为原来的四分之一(长宽都减半),全屏时也只占左上角的四分之一。

这几天我给 xorg-xwayland 打上了新版补丁,然后在一番理解之后,给 wlroots 写好了相应的补丁,现在 Xwayland 终于也可以看清晰了!

这两个补丁,xwayland 那边是通过一个 X 窗口属性来设置缩放倍数,然后 xwayland 会告诉混成器自己的窗口使用了对应的缩放倍数,这样混成器就不会当它不支持缩放、强行给拉伸一下了。当然还有输入坐标的转换之类的。缩放倍数为 2 时,X 客户端会看到之前两倍的显示大小,并且 X 使用的坐标是 Wayland 这边的两倍,所以 Wayland 的输入事件从 X 服务器传给 X 客户端的时候需要乘以 2。

混成器这边,需要了解客户端传过来的 X 坐标和 Wayland 坐标不再相同,需要进行相应的转换。没有进行转换的结果就是,客户端告诉混成器说自己是 1024x1024 的窗口大小,然后实际上创建出来是 512x512 的。混成器再告诉客户端你现在只有这么大,然后客户端说好,我调整一下。于是又变小了……

两个补丁打上之后,由于头文件有变化,混成器可能需要重新编译一遍。然后按 X 的方式设置 HiDPI,比如设置 Xft.dpi: 192 或者 winecfg 里设置 dpi 为 192。如果有运行于 Xwayland 的 GTK 3 程序,也要设置 GDK_SCALE=2 GDK_DPI_SCALE=0.5。然后执行以下命令设置 X 属性,让 Xwayland 做相应调整:

xprop -root -format _XWAYLAND_GLOBAL_OUTPUT_SCALE 32c -set _XWAYLAND_GLOBAL_OUTPUT_SCALE 2

接下来就能愉快地 wine 和 gimp 啦~

这几天做的另一个比较大的动作是配置好了 waybar。我也不知道这个组件叫什么好,很多地方都叫面板(panel),i3 / sway 那边直接叫 bar。它现在经常出现在屏幕顶部所以我也有时叫它顶栏。总之就是显示窗口信息、系统托盘和状态指示器啥的那一条。

我是从它自带的配置文件修改的,但是风格给完全改掉了。原本的风格是一块一块的彩色背景的字,我嫌太过显眼,经过一番调整之后,给改成了黑底上的彩色字,跟我原来的 Awesome 差不多,也和我显示器的黑边挺配的。指示器的放置也是差不多的。十分遗憾的是并没有合适的窗口列表小部件可用。它自带的那个 wlr/taskbar 会把所有工作区的窗口全部显示出来,很挤。而且不知道为什么,一旦加上它之后就最小要占用 34px 的高度,太占空间了,所以作罢。我打算以后自己实现一个,现在就拿正在播放的媒体凑一下吧。它现在长这样:

我的 waybar

左边就是使用 playerctl 做的媒体信息。左键可暂停播放,滚轮切歌。字的颜色是和窗口边框匹配的。

中边留着给窗口标题。

右边依次是:

  • idle 禁制器。点一下眼睛亮起来,禁用无活动时自动休眠。
  • CPU 和 load 信息。
  • CPU 温度。太高了会变红。
  • 内存使用率。
  • 电池信息。充满电又插着电源线,它就隐藏起来了。预期充电或者使用的时候图标会出现,并且可以看到剩余时间。如果在放电并且电量低,应该会变红并且闪烁。这设定跟我 Awesome 的那个一样,不知道等到什么时候才能用上,到时候才能看到效果了。
  • 音量。左键单击是切换静音,滚轮调节音量。图标会显示设备类型(我这里有内建、HDMI(实际上是走的 DP)、蓝牙三种)。麦克风静音的时候也会显示图标。
  • 系统托盘。它终于可以在多个屏幕上同时显示啦~
  • 时钟。

我最终还是用上了那个包名为「otf-font-awesome」的图标字体。

waybar 比 Awesome 的 bar 好配置多了。很方便使用外部程序来定义。也不一定要用定时器。程序可以一直跑着,一行一条状态更新,就不需要在「更新不及时」和「更新太频繁消耗资源、干扰调试」之间抉择了。

我把 wayfire、waybar 以及其它一些东西的配置文件上传到 GitHub dotconfig 仓库了。XDG 标准路径挺好的。

还有一些小的更新——

壁纸我使用了 swaybg,因为它支持不同显示器使用不同的壁纸,这样我的 e-ink 墨水屏就可以独享纯白壁纸了~或者什么时候我专门找张合适的黑白壁纸也挺好的。

fcitx5 输入条的文字 padding 太大。鉴于我现在日常使用 Wayland 了,我改了我这个主题,适配 Wayland。X 那边也没有太难看。

fcitx5-paste-primary 已经添加了 Wayland 支持,虽然实现很不优雅……

看图软件,使用 imv(同时支持 Wayland 和 X)取代了 sxiv。许多图片要预览的话,thunar 或者 geeqie 也挺好的。

以及一些已经报告的 bug:

还有一些未报告和未调查清楚的 bug,等事情明了之后我再更新啦~

by 依云 at November 20, 2021 01:45 PM

November 15, 2021

百合仙子

Wayfire 迁移进展

本文来自依云's Blog,转载请注明。

这几天又解决了一些问题,记一下。

Wayfire 部分:

  • 部分按键绑定无效的问题,Super+数字键无效是因为这个是 git 版本才有的。PrintScreen 无效是因为需要写成KEY_SYSRQ……
  • 窗口标题的问题。显示中文的 pr 已经提交。也把 scale 插件的问题一起修了。
  • 要不显示标题栏也很好办,首先 preferred_decoration_mode = server 让窗口们都用 wayfire 画的装饰,然后设置 height = 0 这样就看不到标题栏啦(窗口边框还留着;连边框都不想要的话可以不加载这个插件就好了)。
  • lightdm 启动不了 wayfire 的问题,在 ~/.xprofilesleep 1 就好了。相关 issue:Missing some input devices in wayland session · Issue #63 · canonical/lightdm
  • 嗯,lightdm 是会给 Wayland 会话 source ~/.xprofile 的。所以在里边判断 XDG_SESSION_TYPE 环境变量然后做相应的处理就好了。另外 sddm 是会 source ~/.profile 的。
  • invert 和 zoom 插件只支持一个显示器的问题,没有再次复现。可能是 git 版修了吧。
  • 挂起之后恢复,没键盘鼠标的问题大概也好了?今天我只遇到过一次没键盘,重新插了一下……
  • resize 调整窗口时保持比例(比如用于 scrcpy)。我已经在自己的 fork 中加入这个功能。
  • 锁屏使用 swaylock。不过它只锁屏并不会关显示器,所以我又写了个 xset dpms force off 的等价程序
  • HiDPI 下 Xwayland 窗口是糊的。通过改变插值算法(默认的 GL_LINEAR -> GL_NEAREST)来缓解。sway 默认就支持这个。这个 nearest 算法在整数倍放大时,会不那么糊,不过颗粒感会很明显(就是把显示器分辨率给降回去啦)。
  • 哦,还有个 git 版本的新问题:slurp 或 swaylock 在运行时,会消耗不少 CPU。我发现是由于 wayfire 一直在发送 configure 事件造成不断地重绘,已经给补上并提交 pr 了。

我的 Wayfire fork 位于 https://github.com/lilydjwg/wayfire/tree/lilydjwg,里边有什么请自行看提交历史。不过要注意的是,这个分支我可能会 push -f 以清理历史。

应用程序部分:

  • flameshot 需要设置 XDG_CURRENT_DESKTOP=sway 才能工作,然而在多显示器的时候只会给用户编辑左上角的部分,还是没法用。于是我用回传统的「选择+截图」组合了,只不过在 Wayland 下是 slurp + grim 这个组合。
  • wl-paste 和 xsel 不同步的问题,是由于 wlroots Xwayland 在窗口没有焦点时,被禁止与 Wayland 部分同步剪贴板。
  • 我装好 xdg-desktop-portal{,-wlr},设置好 XDG_CURRENT_DESKTOP 环境变量,然后重置了一下 obs-studio 的配置文件之后,它能工作了。不过只能录整个屏幕,不能按窗口录啦(由于更容易意外录到别的内容,反而不那么安全了)。

然后是剩下的问题:

  • spicy 无法捕获键盘是因为 wayfire 没有实现那个协议。有空我去 patch 一下好了。
  • 火狐还是不能录屏。听说是协议有更新火狐还没跟上?
  • wireshark 的菜单会显示在屏幕最右边。
  • mako 的通知文字经常是糊的,reload 一下什么的可能会好。也遇到过它不显示,reload 一下又好了的情况。
  • GTK 3 程序的右键菜单,上下会多出一部分内边距并被加上的圆角。圆角我还能忍,但多这么一部分不能选中就很难看了,然后火狐的多级菜单还没把这个考虑进去,没对齐各级菜单……

解决这各种问题挺累的,我就不仔细核查和整理了。本文只是个记录,把已经完成的事项从我的 TODO 列表转存到博客而已啦=w=

by 依云 at November 15, 2021 11:36 AM

November 14, 2021

Lainme

在Centos7/8上使用HillstoneVPN

公司内网使用的是Hillstone的SSL VPN,官方的Linux客户端只支持非常有限的几个老发行版,连Centos7都不支持。不过有些发行版并非真的不能用,只是程序里对发行版做了判断,不是指定的几个就强行退出。

后来我发现有人做了一个移除了版本限制的客户端,在Centos7下面可以完美运行:https://github.com/JackMoriarty/HillstoneVPN/releases

但在Centos8上运行时总是报错,

Unit dbus-org.freedesktop.resolve1.service not found

这个报错的服务是systemd-resolved的,默认没有开启。但我启动systemd-resolved服务后,虽然不再报错,VPN连接后却完全无法上网。搜索后才知道systemd-resolved更改了很多原本的机制,VPN软件必须做相应的修改才能使用。那么怎么才能不用systemd-resolved,又不让客户端报错呢?我花了几个小时终于发现了最简单的解决方法,删除/sbin/resolvconf

虽然客户端可以用了,但我对这个客户端很不满意:

  • 无法做成systemd服务开机自动启动和连接指定的VPN
  • 需要在桌面持续运行,无法很好的最小化

经过一些搜索后,我发现了一个第三方的命令行客户端Hilldust (https://github.com/LionNatsu/hilldust),基本能解决上述问题,但它也有自己的一点不足

  • 不支持从配置文件读入参数,在多人使用的电脑上会暴露密码
  • 没有从VPN服务器返回的路由表中设置路由,对我们公司的网络无法设置正确的路由
  • 不能很好的作为systemd服务运行,主要是退出时的判断仅有KeyInterrupt,没有考虑SIGNAL。
  • 使用iproute2来配置网络,可能和NetworkManager冲突。另外网络复位主要靠save & restore,如果在VPN运行期间还做了其他网络的调整,复位的时候也会回退这些调整。

于是我在Hilldust上重新做了一个Wrapper,处理了一下这些地方,在Centos7/8上都测试通过了。如果你也需要的话,可以试试:https://github.com/lainme/hilldustWrapper

by lainme (lainme@undisclosed.example.com) at November 14, 2021 12:52 PM

November 12, 2021

百合仙子

不同情况下的图形效果

本文来自依云's Blog,转载请注明。

我在上一篇说到,Wayland 下再也不会遇到撕裂了。后来群友说,这是 Intel 的 modesetting 驱动特有的问题。所以我又重新比较了一下。为了让事实说话,而不是靠很容易有偏见的主观感受,我使用 Sony Xperia XZ2 Compact 手机的「慢动作」功能,以 960fps 录制了不同情况下,火狐滚动同一页面的效果。不过视频我就不放了,上传费时又占地方。

首先是我长期使用的组合:X11 + Intel 集成显卡 + modesetting DDX,窗口管理器是 Awesome 3.5.9,混成器是 picom。这种情况下有非常严重的斜线撕裂,在画面内容变化大的时候(如无过渡切桌面、视频镜头转换、大幅度滚动页面)必现。撕裂的样子如下(「慢动作」的分辨率有限,亮度低是预期现象):

X11 Intel 上的画面撕裂

可以很清楚地看到,截图中的页面是由两帧的内容拼接而成的。而且左下的是后一帧,右上的是前一帧,我不知道为什么会是这样子。我记得以前它不会撕这么大条斜线的啊,是斜-水平-斜的样子。反正都很难受就是了。我以前以为这种撕裂是时不时出现的,但多次「慢动作」慢放表明,它发生的频繁度大大超出了我之前的感受。

这个组合还有个问题是:外接显示器时,鼠标会跟着画面的更新而闪烁。画面更新越多越频繁,它闪烁得越快,而且和更新的区域也有关系。因为眼睛总跟着鼠标跑,所以即使它大部分时候不影响定位,但对主观感受的伤害应该也挺大的。

然后 Wayland + Intel 集成显卡,混成器是 Wayfire。完全观察不到撕裂。鼠标光标也很稳定不闪。

X11 + Nvidia 独立显卡 + 官方闭源驱动。还是 Awesome + picom 的组合,输出还是经过 Intel 显卡。也完全观察不到撕裂。不过有个更严重的问题——我的外接显示器每十来秒会黑屏几秒。听说把显示器关掉重开就可以解决,不过我无意使用这个方案,就不理它了。除了我之前提到的容易崩溃之外(其实我也不知道现在它还崩不崩),这显卡不支持视频硬件加速。这意味着我根本没办法看 4k 视频。PS: 火狐的 WebRender 能正常自行启用。

X11 + Intel 集成显卡 + Nvidia PRIME Offloading。我只是好奇地试一试这个方案啦。以前这个方案里火狐没办法启用图形加速,只能用「basic」渲染器来着。现在火狐有了 Software WebRender,倒是也能比较好地跑起来了。依旧会撕裂,好像比用 Intel 显卡要好那么一点点?

然后对比一下水族馆的图形性能数据:

  • X11 + Intel 显卡,30fps 左右。
  • Wayland + Intel 显卡:接近 60fps。
  • X11 + Nvidia 显卡:30-40ftps。
  • X11 + PRIME,这个比较令我意外,竟然也在 30fps 上下。要知道这个是纯 CPU 实现的,没有 GPU 加速的呀。

其实 WebGL 我用得不多啦(Google 地图更常卡在网络 I/O 上而不是渲染上)。更多的是播放在线(YouTube)视频啦。

  • X11 + Intel 显卡,1080p 60fps,GPU 用满,丢帧三分之一!4k 60fps 也差不多。原来重点不是分辨率(反正 GPU 的解码能力还没用满),重点是视频的帧率啊。
  • Wayland + Intel 显卡,4k 60fps 都不怎么丢帧,更不说其它了。GPU 图形计算用到一半左右。
  • 没有更多方案了。我这 CPU,软件解 4k 会卡成 PPT 的。

综上,Wayland 的表现是最好的!好吧虽然遇到了挺多问题的,不过我大概都能修或者绕过。虽然撕裂不全是 X11 的错,但是确实有客观证据证明它体验不好,不是 Wayfire 的特效太绚烂太怀旧让我偏心了~

by 依云 at November 12, 2021 01:27 PM

November 09, 2021

Alynx Zhou

把我的时间戳还给我!

当然,我在标题里说的时间戳并不是狭义的 UNIX 时间戳,我只是想表达一下精确的时间记录而已。

不知道从什么时候开始,许多网站都不再显示 2021-11-09 09:00:00 这样精确的时间,而是开始显示“刚刚”、“5 分钟前”、“3 个月前”、“1 年前”,我能猜出来这又是哪些自以为聪明的产品经理以“对用户友好”的理由想出来的或者抄来的,但不幸的是大部分这种行为都很愚蠢。据我所知 Twitter 是这样,Twitter 做什么就抄什么的微博也是这样,令人难以忍受的是 GitHub 也是这样显示时间,以及今天彻底惹恼了我的 YouTube 在视频页面也是这样显示。

如果你还没搞清楚我的愤怒来源,我实际上是想做这样的事情:我知道索尼 A7S3 是在 2020 年 7 月发布,我想快速找到相关的视频,请问我怎么在这一大片的“1 年前”里猜到去年 7 月的大致位置?

全都是 1 年前

“1 年前”这个描述可以包含从“1 年零 1 天”到“1 年零 364 天”这样不精确的时间范围,我觉得和直接不显示时间范围也没什么区别了。或者像 GitHub 那样鼠标悬浮在上方显示时间戳的方案也不好,比如这个页面这么多视频,我的目的是一眼快速找到我需要的时间段,一个一个悬浮多慢?所谓的用户友好?不见得,用户要被气死了。人类的脑子还没弱到连算个大致的时间差都算不出来。从工程上来说,这还引入了额外的复杂度,比如有一个 JS 脚本是专门用来更新这些时间的,你需要引入额外的代码,才能保证从“刚刚”变成“7 秒前”、“10 秒前”、“不到一分钟”、“5 分钟前”(说的就是你 GitHub),哈哈,这简直太好笑了。更别提弱智的微博 API 竟然直接在 JSON 里面返回“X 分钟前”,不过反正这种互不联网说不定什么时候就把 API 给砍了。

我觉得 Twitter 和微博这种本身就是短平快讲究时效性的网站使用大致的时间差而不是时间戳还算可以理解,至于 YouTube、 GitHub 这种经常需要查看精确时间的显示大致的时间差纯粹是给用户填堵,这一点 B 站经常脑残的前端反而做得不错,在视频页面显示的是精确的日期。

by Alynx Zhou (alynx.zhou@gmail.com) at November 09, 2021 01:19 AM

NVIDIA 驱动和 GNOME 和 Wayland

更新(2021-11-09):最新的 NVIDIA 495 驱动终于支持了大家都在用的 GBM,同时最新的 XWayland 21.1.3 也添加了这方面的支持,也就意味着 NVIDIA 用户不再需要单独的轮子而是使用现有的稳定的代码。我已经切换到 GNOME Wayland 不止一周,目前各种功能都很正常。


由于各种各样的历史原因和近期的变化,我在最近的聊天里发现很多朋友对 NVIDIA 驱动对 Wayland 的支持情况不甚了解,正好我最近在折腾相关的东西,所以打算简单介绍一下我了解的。

常见问题

为什么 NVIDIA 的 Wayland 支持这么差?

长话短说,大部分驱动(AMD 的开源和闭源驱动,NVIDIA 的开源驱动 nouveau,Intel 的开源驱动)都同意采用 GBM 作为缓存 API,但是 NVIDIA 像个赌气的小孩一样表示“你们这个不好,我要做个更好的 EGLStreams”所以不支持 GBM,于是所有要兼容 NVIDIA 闭源驱动的混成器都要为它做单独的实现代码。诸如 GNOME 或者 KDE 这种大型项目有足够的人手,同时 NVIDIA 似乎也提供了一定的帮助所以已经有了混成器内部的 EGLStreams 实现,但也有些小型项目并不支持这个玩意。而且 NVIDIA 吹牛皮吹得很大,EGLStreams 哪里“更好”从他们的龟速进展上根本体现不出来。可能是因为挖矿赚得盆满钵满,忘记了显卡的本职工作是显示,NVIDIA 的闭源 Linux 驱动质量一直停留在能用的程度,同时又做各种各样限制导致开源的 nouveau 在较新的显卡上几乎不能用。

NVIDIA 闭源驱动之前一直是稀泥巴糊不上墙的垃圾,因为它在 Wayland 下对使用 XWayland 的程序没有硬件加速支持——也就是说如果你使用 Xorg 会话,程序是有硬件加速的,你使用 Wayland 会话,Wayland 程序也是有硬件加速的,但是在 Wayland 会话下面那些旧的 Xorg 程序就没有硬件加速(听起来就像捡了芝麻丢了西瓜一样)——而仍然有相当一部分旧程序在 Wayland 下依赖 XWayland 运行,所以 NVIDIA 闭源驱动的 Wayland 会话基本可以认为是不能日常使用的。

从版本 470 之后 NVIDIA 闭源驱动有所改善,首先是他们的员工提交的补丁在 XWayland 21.1.1 版本释出了,这个补丁添加了对 XWayland 硬件加速的支持(需要新版本驱动),然后新版本驱动在上周变为稳定版,用户可以安装,所以在拖了至少三年之后最后一块空缺终于被补上了。使用 KDE 和 GNOME 的 NVIDIA 显卡用户理论上就可以获得完整的 Wayland 体验了。

现在 NVIDIA 用户的 Wayland 会话可以日常使用了吗?

更新到 470 驱动之后我第一时间体验了一下,结果就是很抱歉,还是不行,不过 我遇到的都是一些小问题:

  • Firefox 稳定版没法正常工作,但是 Nightly 版本是可以的,原因是 NVIDIA 和 Mesa 的 EGL 实现存在一点小区别,修复补丁已经在 Nightly 里面了,我们只要等稳定版版本升上去就可以了。
  • GTK3 程序都能工作,但是 GTK4 的程序(比如 GNOME Extensions)却全都挂了,原因应该和 Firefox 一致,修复补丁也已经合并了,只是还没有释出稳定版。
  • Google Chrome 不能工作,但是比较新的 Chromium 似乎可以,原因推测也是一样的,我懒得查了,同时 所有一直抱着旧版 Electron 不更新的程序应该都有同样的问题。
  • GNOME Shell 的缩略图会显示错误,看起来是个两年的老 bug,最近开发者似乎找到了原因,还没修复。

不过总而言之这些都是能很快修复(废话,大家动作都比 NVIDIA 快,建议老黄把自己的皮衣换成乌龟壳)的问题,不至于被卡很久,至于你的发行版是那种追求“稳定”选择了一个不支持的大版本然后不更新的?哈哈,关我 X 事。

更新(2021-09-17):Arch 这边 Firefox 更新到了 92.0,所以已经修好了。Google Chrome 现在是 93.0.4577.63,我也能正常打开。GTK4 的 MR 已经释出了稳定版,所以没问题。Atom 在我这用的是 electron9,能用,反而是用 electron12 的 Code - OSS 不能用了……最后那些不知道打包了几百年前的闭源拖拉机(要不把拖去掉变成垃圾?)也就是我指的很明确的 Slack 不能用,加上 --disable-gpu 是能用的,但是是他的问题,为什么要我关掉硬件加速消耗 CPU 资源?最后 GNOME Shell 自己的那个 bug……目前没有开发者搞这个,我简单看了一下发现是个我没涉足过的领域,不过总之我觉得是个看起来不爽但不影响你实际使用的问题,所以现在的状态就是 “又不是不能用” !(我可没说后半句啊!上一个说了后半句的珠海小厂现在还活着吗?)

另外我在 openSUSE Tumbleweed 上测试的时候还遇到一个问题啊,它默认安装的是 nouveau,需要手动在 YaST 里面安装 NVIDIA,装完之后似乎两个驱动冲突把我显示器搞黑屏了,于是我就没法直接重启。而且在那之后我显示器神奇的再也不亮了(PS4、Switch 全都点不亮它),拔掉电源放半分钟再插上才好,很头疼啊。

我是 GNOME 用户,为什么装了 470 版本 GDM 选单里的 GNOME 还是 Xorg 会话?

这个有几个原因,首先就是 Wayland 需要 Kernel Mode Setting 的支持(这也是个 NVIDIA 拖了好久才补上的特性),所以你的内核参数要有 nvidia-drm.modeset=1。其次要看 /etc/gdm/custom.conf 里面 WaylandEnable=false 是不是被注释掉的(默认应该是)。

最可能的是 udev 规则,GDM 带了一个 /usr/lib/udev/rules.d/61-gdm.rules 文件,里面有这么几行:

# disable Wayland on Hi1710 chipsets
ATTR{vendor}=="0x19e5", ATTR{device}=="0x1711", RUN+="/usr/lib/gdm-runtime-config set daemon WaylandEnable false"
# disable Wayland when using the proprietary nvidia driver
DRIVER=="nvidia", RUN+="/usr/lib/gdm-runtime-config set daemon WaylandEnable false"
# disable Wayland if modesetting is disabled
IMPORT{cmdline}="nomodeset", RUN+="/usr/lib/gdm-runtime-config set daemon WaylandEnable false"

第二行表示如果检测到 NVIDIA 闭源驱动就关闭 Wayland,第三行则是如果没开启 Kernel Mode Setting 也关掉 Wayland。

我猜大部分用户都没有第一行的设备,所以我建议直接 ln -s /dev/null /etc/udev/rules.d/61-gdm.rules 屏蔽掉这个文件。

别问我既然 470 驱动都出来了 GNOME 开发者为什么不删掉这个文件,NVIDIA 憋出来 470 驱动总共也就一周多点,还有一堆小问题呢,删了怕不是又有一堆人来骂“怎么又把不稳定的东西搞给我了”。

我是笔记本双显卡用户,主用的是核芯显卡,为什么我 GDM 选单里的 GNOME 也是 Xorg 会话?

检查一下你是不是用了 NVIDIA 之前提供的 Xorg 下面的双显卡管理功能(有群友说叫 prime,我不知道是不是叫 prime)(顺便一提这也是 NVIDIA 憋了好几年才憋出来的本来就该有的东西)?如果你使用那个,首先它是 Xorg 下面的支持,其次它会加载 nvidia 模块,自然就会落到上一个问题里面的 udev 规则上。

有群友在群里表示他删了那个规则然后进 Wayland,可以是可以,但我感到很迷惑。这个 prime 功能 NVIDIA 之前只是说了在 Xorg 下面可用,所以我不是很理解又要 prime 又要 Wayland 的可行性,我也不知道 NVIDIA 的按需渲染现在到 Wayland 里面还能不能像之前 Xorg 里面提供的功能一样正常工作。就理论上我给 NVIDIA 掏了钱,这是他的义务给我支持这个功能,但这可是垃圾 NVIDIA 啊!

你问我有没有什么建议?如果你需要这玩意,就按照 GNOME 的规则用 Xorg 就好了,没错虽然我是 Wayland 支持者,但我在这里建议你还是先用着 Xorg。如果你只是想用 Wayland 然后并不是很需要用 NVIDIA 显卡,那就关掉它不让它加载驱动,没错,我在自己的笔记本上用的是 Bumblebee。

我是 KDE 用户,你扯这么多 GNOME,跟我说说 KDE 现在有什么问题呗?

大哥,我是 GNOME 用户啊,既然你是 KDE 用户,建议你自己试试然后给我讲讲……

没用的观点

你看这个 GDM 就是逊啦,我这里卡死根本不能用,大家都推荐换 SDDM 巴拉巴拉……

强调一下我的目的是解决问题,比如群里有人问为什么我双显卡 GDM 里没 Wayland 可选你来这么一句我觉得很不礼貌,当别人就是需要用 GDM 的时候说“大家都推荐换 SDDM”我觉得不管是 GNOME 用户还是 KDE 用户都会觉得不是个好回答。其次只说自己 GDM 卡死完全没法判断问题所在,别人都没这个问题,或许问题就不在 GDM 而在你的配置呢?同理,这个“大家都推荐换 SDDM”里的“大家”是哪里来的呢?我觉得这样的回复对解决问题没什么帮助。

Xorg 好!Wayland 坏!

请列举出充分而恰当的理由支持你的观点,而不是人云亦云。比方说你可能觉得 Wayland 缺乏你需要的某些功能,这可能是设计理念上的不同,或者发展时间不够所导致的,毕竟 X 协议的历史比 Wayland 长得多,所以请多一点耐心。而至于不讲道理或者二极管或者言论不友善的行为在哪里都是不受欢迎的。

我也是 NVIDIA 用户,我玩 XX 游戏卡成幻灯片了!

……信息太少,无法给出合理推断,只能告诉你我游戏玩的可欢乐了!

你们 NVIDIA 用户真麻烦,谁叫你们自己掏钱买 N 卡受罪!

你又不知道老子都有什么需求,买 N 卡我是暂时没法用 Wayland,但是不买 N 卡我就要达芬奇受罪 Windows 下面打游戏受罪直播编码受罪跑 hashcat 受罪(存疑)。我做的选择是我权衡之后的结果。再说一遍,解决问题和“换 XX”是完全不一样的态度。

所以就是钱是掏了,骂还是照样骂,而且我都掏钱了,骂得然更有底气了!

by Alynx Zhou (alynx.zhou@gmail.com) at November 09, 2021 01:10 AM

November 08, 2021

Phoenix Nemo

笔记:Arch Linux iPXE 基本启动脚本

似乎一直没有(公开且版本够新)的 Arch Linux 无人值守安装配置,所以想做一个。

参考 netboot.xyz 的 iPXE 配置,简单记录一下:

1
2
3
4
5
6
7
#!ipxe

set mirror http://archlinux.mirror/archlinux/iso/latest
set script http://unattended.install.script/script.sh
kernel ${mirror}/arch/boot/x86_64/vmlinuz-linux archisobasedir=arch archiso_http_srv=${mirror}/ ip=::: BOOTIF= net.ifnames=0 script=${script} mirror=auto
initrd ${base-url}/arch/boot/x86_64/initramfs-linux.img
boot

参考 SYSLINUX 的 PXELINUX 部分和内核 ipconfig 部分的文档写 ip= 参数的时候一边拿不到 DNS 一边疯狂报过多参数,最后发现是这个 bug 的锅。而且它还被 netboot.xyz 在脚本里注释出来了我可能需要去检查一下视力

总之目前暂且只能用 DHCP 来在启动过程正确配置网络,否则无法下载系统镜像。

截至写这篇的 Arch Linux 的官方说明在这里

最后就是自动安装脚本了,这个坑暂且撂在这里,以后有时间了再慢慢填…

咕咕咕

似乎一直没有(公开且版本够新)的 Arch Linux 无人值守安装配置,所以想做一个。

参考 netboot.xyz 的 iPXE 配置,简单记录一下:

November 08, 2021 07:16 PM

百合仙子

Wayland 初体验

本文来自依云's Blog,转载请注明。

前天得知 wayfire 能够直接在 X Window 下运行之后,我就尝试了一下。很好,能跑。虽然不能捕获键盘,从而与外边窗口管理器重复的快捷键无法使用,但至少它的鼠标光标位置是对的,能够用来测试。我在 QEMU 里跑过 KDE Wayland、sway 和 wayfire,无一例外鼠标位置不对。KDE 里是缩放倍数设为 2 之后,鼠标的实际坐标会翻倍。后两者基于 wlroots 的,鼠标的实际坐标位于显示的光标上方几十像素处。总之没法用。

在 X Window 里粗略地配置了一下 wayfire 之后,我就想实际在外边跑跑看。结果被惊艳到了,感觉就像年轻了24岁一样!

优势

尝试几次之后,我终于把 wayfire 跑起来了。

首先是火狐图形性能翻倍!

我打开火狐,跑了一下 WebGL 水族馆(微软的 FishGL 没啦)。接近 60fps,是我在 X Window 下跑的两倍有余!再试试 YouTube 的 4K 视频,竟然几乎不掉帧!我的电脑的性能原来这么好的吗……在 X Window 下,火狐播放这 4K 视频,会占满 GPU,并且 CPU 也几乎用满,丢掉约十分之一的帧。而在 Wayland 下竟然只用了一半的 GPU,CPU 用量也不怎么高。而且操作起来很流畅,右键菜单一点就出来,鼠标划过菜单项,高亮也跟得很紧。

画面再也不撕裂啦!

一直以来,我切换窗口、播放视频、快速滚动窗口内容的时候,时不时能见到画面撕裂。一般是在中间附近,一道水平接着倾斜接着再水平的裂痕会一闪而过,其上和其下是两帧不同的内容。读过 xplain 以及 Wayland Book,我知道这是由于 X Window 服务端与客户端之间并不同步,显示画面时,缓冲区里有啥画啥,并不在乎客户端有没有画完,导致相邻的两帧拼接在了一起。而 Wayland 会使用双重缓冲,客户端在缓冲区里画好了,才告诉混成器这一帧画好了、可以显示出去了。而在画好之前就显示上一次提交的缓冲区内容,所以永远不会出现画一半的情况。

我的光标也不再闪烁啦!

我也不知道为啥,我在 X Window 下,特别是接外接显示器的屋里,鼠标光标会像遥远的星星一样闪啊闪的,有时候闪得甚至影响我操作。我用的 picom 混成器的选项我改来改去,不光没能解决这个问题,反而是搞得它时不时崩溃一下了。我也不知道这是怎么回事,按理说鼠标光标由硬件绘制的,不应该时有时无啊。而在 wayfire 里,光标一直很稳定,从来不闪烁。我是返回到 X Window 下才注意到这一点的。

特效!

尝试 wayfire 的一大原因是我看中了它的特效。当年我初入 Linux 的时候,就挺喜欢 compiz 的特效的,只是后来因为性能问题才转到 Awesome 的。wayfire 有好多我当年常用的特效,果冻、火焰、立方体、展览,还有那个翻页一样的切换效果叫啥来着。没有纸飞机倒是有点可惜。

其它的 Wayland 混成器,KDE 我已经尝试过了,虽然是 X Window 版本,但功能应该是一致的。同样 sway 的对应 X Window 版本 i3 我也尝试过了。结论是,我并不是很在意平铺,但是我很在意键盘操控性。wayfire 也有较为丰富的键盘操作设置,应该也可以通过插件来扩展。

已解决的问题

虽然有上边这些优势,我还是回到 Awesome 来了,因为还有不少问题和需要配置的地方呢。此节记录一下我已经解决的问题。

  • GVim。因为 GVim 使用的是 GTK,所以改一改让它支持 Wayland 并没有很难。我改好的版本在我的 vim fork 的 wayland 分支上。目前还缺少两个重要功能:终端里的剪贴板,以及 +clientserver。
  • mpv 是糊的。研究发现这是因为我加了no-hidpi-window-scale参数。它是为了解决 X Window HiDPI 下窗口过大的问题,但到了 Wayland 下使用它就会导致并不进行 HiDPI 缩放了。
  • 窗口焦点不会跟随鼠标。这是在平铺式窗口管理器里使用很广泛的特性。wayfire-plugins-extra 里有个 follow-mouse 插件可以实现这个。
  • GTK 的鼠标主题、禁用动画设置等不生效。原来在 Wayland 下 GTK 不读取~/.config/gtk-3.0/settings.ini,可以使用 dconf-editor 去编辑 org.gnome.desktop.interface 下的选项
  • xfce4-notifyd 的通知会被当成普通窗口,置于屏幕中间并且有标题栏。有一个 fork 修复了此问题,不过我还是决定使用更轻量的 mako(不是 Python 的那个模板引擎哦)。
  • Xwayland 跑 PRIME offloading 时,Minecraft 的画面会来回抖动。于是只好放弃 NVidia 显卡改成 Intel 显卡了(反正自从我买了 4K 显示器之后就都跑不动了 QAQ)。
  • fcitx5
    • GTK 中,在输入编码的时候,按一次键闪烁一下。csslayer 说是不这样做就无法移动窗口。于是我改了一下代码,选择移不动。反正也只有在屏幕边缘的时候才需要移动。Qt 那边这个问题倒是不大,因为 fcitx5-qt 总是在应用程序的窗口范围内绘制候选词窗口,能够判断什么时候需要移动,所以只是在屏幕边缘的时候闪一闪。
    • Qt 中(就是 Telegram 啦),在窗口下方输入的时候候选词窗口会悬在文本上空。我改了一下代码,现在能够准确定位到文本的上方紧贴着了。代码已经 pr。
    • fcitx5-configtool 不显示部分图标。把GNOME_DESKTOP_SESSION_ID=0环境变量加上就好了。这个环境变量莫名其妙地解决了不少问题呢。

剩下的问题

我暂时还是以 Awesome 为主,因为还有很多问题有待解决:

  • Vim 不支持剪贴板、clientserver 特性。我打算通过另外的方式实现。工作量有些大。
  • SPICE 客户端 spicy 无法捕获键盘,造成虚拟机里无法使用与外边相同的快捷键。
  • LightDM 启动 wayfire 会话时会立即退出,而实际上 wayfire 是已经启动了的。各种日志里均没有找到任何有用的信息。
  • wl-paste 获取的剪贴板内容与 Xwayland 的并不同步。似乎大部分程序都支持两种剪贴板协议,但是 wl-paste 和 Xwayland 各支持其中之一?
  • Xwayland 不支持 HiDPI。有个补丁,但是会造成窗口一直缩小再缩小。
  • fcitx5
    • 候选词的内边距有些大。有空再调了。
    • fcitx5-paste-primary 不支持 Wayland。有空再加了。
  • wayfire
    • 窗口标题不支持中文。我倒是打算给 wayfire 加个不显示标题栏的选项。
    • 的 fast switch(真的很快)会导致全屏窗口取消全屏。
    • 似乎不支持触摸板拖拽的时候中断一下。应该很好修补吧?
    • 只能切换到本工作区的上一个窗口,不能跨工作区
    • invert 和 zoom 插件只支持某一个显示器。前者我用不到,但后者应该还有些用处的。
    • 设置的 Super+数字键 切换工作区无效。
    • 我还没想好我那些环境变量去哪里设置比较好。~/.xprofile没啦,~/.pam_enviroment我也不想用。在考虑使用 systemd 那个功能,或者我自己 wrapper 一下 wayfire。

缺憾

  • 使用 libinput 取代了 synpatics,非常好用的画圈滚动(一直画、一直滚)没了。

结语

还有挺多东西要配置的。锁屏啦,壁纸啦,还有未发现的问题啦,等等。慢慢来吧。Wayland 真的挺顺滑的。

对了,有个有意思视频给大家看一下。

火狐着火啦~

这是我还没运行通知守护进程时,火狐发出的通知窗口,不知道为什么被反复创建和销毁。

by 依云 at November 08, 2021 12:55 PM

November 07, 2021

Lainme

Linux系统迁移

最近换了一台新电脑,同时买了块NVME SSD来替换以前的SATA SSD,需要把以前的系统迁移过去。查了一些资料,基本方式就是dd和rsync两种。因为rsync更加灵活,所以最后选择了rsync的方式。

由于采用了rsync的方式,新的SSD需要手动进行分区。我是参照以前的分区做的,还是划分成一个/boot/efi启动分区,一个/根目录分区和一个swap分区。

分区完成后需要将新的分区都挂载上,比如新硬盘是/dev/nvme0n1,其中的/boot/efi分区是/dev/nvme0n1p1,/分区是/dev/nvme0n1p2,那么

sudo mkdir -p /tmp/nvme/boot/efi
sudo mount /dev/nvme0n1p1 /tmp/nvme/boot/efi
sudo mount /dev/nvme0n1p2 /tmp/nvme

然后就可以传数据了。命令来自ArchWiki,你可以参照着进行修改。

rsync -aAXHv --exclude={"/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found"} / /tmp/nvme

数据传输完后需要将/tmp/nvme/etc/fstab里的UUID改称新分区的。UUID可以用blkid命令来查询。

完成后需要chroot进新的系统来重做引导。先bind一些必要的目录,

sudo mount --bind /dev /tmp/nvme/dev
sudo mount --bind /proc /tmp/nvme/proc
sudo mount --bind /sys /tmp/nvme/sys

如果是CentOS7,还需要额外执行

sudo mount --bind /run /tmp/nvme/run
sudo systemctl start multipathd.service

然后Chroot

sudo chroot /tmp/nvme

引导的重建命令根据引导方式(MBR/EFI)和不同发行版可能不太一样,这里记录的是CentOS7重建EFI引导的命令。因为硬件有些更改,最好先重新生成initramfs/initrd,再重建引导记录

sudo dracut -f
sudo grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg

by lainme (lainme@undisclosed.example.com) at November 07, 2021 11:05 PM

November 06, 2021

frantic1048

在 Arch Linux 上使用 LilyPond 记录鼓谱

最近许久没打 OSU! 了,一方面是感觉手指手腕很累(单手 XZCV 默认键位太鼓模式受害者),再玩下去迟早要完,另一方面是摸了个太鼓控制器来玩也感觉不是很带感,又不想止于听音乐,不如尝试一下真的乐器吧,所幸遇到了很棒的老师,摸到了爵士鼓的大门。

作为听说读写学习的一部分,同时也是为了能理解和学感兴趣的歌,以及基本的交流需要,记谱是必要的一环。为了在电脑(Arch Linux)上记谱,需要能满足这些预期的软件:

  • 能输出常见的五线谱格式的鼓谱,pdf 或者任意矢量图片格式都行
  • 能打一些简单文字标注
  • 能听个响,生成任意音频文件

经过一波大浪淘沙,得到如下两个选择:

  1. MuseScore
  2. LilyPond

两者尝试一番下来,简单来说,MuseScore 和 LilyPond 两个方向的体验就像 Word 和 LaTeX 的区别,两方表达力都很强,功能也都很丰富,主要是操作上风格相差很大,个人口味原因偏好 LilyPond 一些。

混沌的鼓谱

这里有必要提一下前面预期里的「常见的鼓谱」到底是什么,因为直接影响到了后面两个方案的体验。

第一天学认谱认硬件的时候就了解到了爵士鼓是复合乐器(多种乐器的组合,所以英文直接就干脆叫 Drum Set 或者 Drum Kit 了),因为硬件组合本身不固定,可能会少,也可能有一大堆,可能是量产的乐器,也可能是鼓手 DIY 的神奇装备,甚至是鼓手自己的大腿,另一方面敲同一个东西用各种姿势也会出现不同声音,这些操作不是很好标准化,然后刚好有五线谱这个现成的通用的表达能力也很好的通行格式就用上了,然后把里面的音符重新定义到和爵士鼓相关的乐器上,其它的时值、重复、强弱之类的功能就直接复用了。

实践上来说,鼓谱的单个音符有时候要表示最普通地敲特定乐器(敲军鼓、敲底鼓、敲重击镲、敲大腿……),有时候要表示用特定手法敲特定乐器(敲节奏镲中间、敲节奏镲镲帽、敲张开的踩镲边缘、敲半开的踩镲边缘、用鼓槌槌尖敲军鼓、用鼓棒末尾敲军鼓的鼓框……),有时候要表示敲出什么感觉的声音(重音、鬼音、渐强、渐弱……)。这些东西记录下来会长什么样子没有很统一的标准,因为硬件组合的不确定性,击打方式的不确定性,加上不同的人不同的实际需求和习惯,记录方式都会有差别,只有比较常用的一部分在不同地方写法比较一致,比如第一个军鼓用五线谱第二格的黑圈圈表示。很难去讨论绝对的正确的谱是怎么长的,配合作者给的额外信息比如图例之类的,能解释出原来作者要表达的意思就行,实在读不懂?那就问!

总结自己学过的谱、各个地方看到的谱、以及随手找到的一些各方对鼓谱记法的描述:

符合我的预期的「常见的谱」是把用手和用脚(主要是底鼓和踩踩镲)的部分用两个声部(Voice)记录了,用符杆朝上的音符记录手打的部分,用符杆朝下的音符表示脚打的,这个记录方式在 MuseScore 和 LilyPond 的功能上来说,都是对应的记录多个声部的功能。

MuseScore

MuseScore 由于单一一个软件把记谱、排版、回放全都带了,还有各种预置的乐谱模板,就成了首先尝试的对象。

安装与起手

安装 MuseScore

Arch Linux 官方软件仓库(官方仓库)里就有打好的 musescore 软件包可以直接装。

安装 MuseScore Drumline extension

可以参见上游给的指南:MuseScore Drumline - Download & Installation,简单来说就是启动 MuseScore,打开菜单栏的 Help / Resource Manager,然后在 Extensions 里找到名为 MuseScore Drumline 的扩展,戳它旁边的安装按钮就行了。

起手

正确完成安装之后,在 MuseScore 里创建新乐谱,到选择模板的步骤的时候,可以看到单独的一个名为 MuseScore DrumLine 的分类,其中的 MDL Drumset 就是普通爵士鼓用的乐谱模板。更多的使用介绍可以去 MuseScore 网站上翻。

体验

比较常用的功能基本不看文档也能戳出来,预览功能很棒,排版很好看。

发现鼠标键盘点点点起来不是很直观,经常在同一时刻已经有音符的时候想再插一个音符变成了插进了下一个音符的位置,操作的问题感觉多看看文档应该能熟练。

然后是自动休止符的插入的特性有点让人头疼,我就输入个刚学的动词大词怎么多了那么多休止符呢,这个功能大体上是 MuseScore 会保证每个小节、每个声部的全部音符的时值加起来是刚好一个小节,如果用户输入的末尾还没满一个小结,那么就会有自动的合适的时值的休止符冒出来去「填满」那个小节的时间,如果继续在那个小节插入音符,那么后面的休止符会自动缩短/消失,这个功能本身挺好的,只是有时候不好看,又不能关掉,只能隐藏音符,感觉不是很优雅。

LilyPond

发现 MuseScore 没有完全让我满意的时候,又来尝试了一下另一个比较著名的记谱工具 LilyPond。起初弹琴动听,做饭又好吃的辣师傅有推荐我用这个来打谱,只是被我嫌弃起手麻烦一开始没尝试。实际体验一番下来,和 MuseScore 一样也有一些不满意的地方,但是因为灵活,又是基于纯文本的编辑方式,有变量之类的避免复制粘贴的功能,用着用着就真香了。

安装

Tl;dr

安装下列软件包:

要装什么

LilyPond 是一个排版乐谱的软件,有了它就可以按照 LilyPond 的语法编写乐谱内容,让它排版出乐谱,然后以 PDF、SVG、PNG 等可以查看的格式来输出,它可以通过官方仓库的 lilypond 包安装。

LilyPond 不能输出能听个响的音频文件,但是它能将乐谱输出成 MIDI 格式的音乐。MIDI 记录的是什么时间什么乐器响了这样的事情,没有记录声音,有点像乐谱,用了脑子才能理解,理解了也只能在脑子里响。虽说不能直接聆听,但是 MIDI 格式的音乐可以用 MIDI 合成器(可以是软件或者硬件,后面的提到的都是指软件形式的合成器)将其记录的内容「转化」成能听到的声音,这有点像是让程序来演奏一个乐谱,这里有个问题是程序并不知道什么乐器是怎样的声音,所以 MIDI 合成器在工作之前,还需要知道需要用到的乐器的声音,这就是与 MIDI 合成器一起工作的 SoundFont 提供的信息,它可以看成是程序要「演奏」 MIDI 时候用的乐器。乐谱(MIDI)只是指定了乐器种类,最后演奏出来的声音会随着演奏的人(MIDI 合成器)和乐器(SoundFont)的变化而出现差异,这里我只是想要预览一下 LilyPond 的乐谱,对音质和仿真程度都没有特别要求,搜寻一番 Arch Linux 官方仓库找到的一组选择是:

  • MIDI 合成器:Timidity++,对应仓库里的 timidity++
  • SoundFont:FluidR3,对应仓库里的 soundfont-fluid

由于 LilyPond 是基于纯文本的乐谱格式,和写程序一样,要是有个语法高亮是再好不过的,如果能再随时看到刚刚编辑的输出那就更好了,搜寻一番之后找到的现成的程序是 Frescobaldi,可以通过仓库里的 frescobaldi 包安装。它有语法高亮,有实时预览,有一键播放,很够用了。只是作为文本编辑器,我目前最顺手的是 VSCode,理论上这套功能也能在 VSCode 上糊出来,获得更好的文本编辑体验,就差一位勇士去填这个坑了:p

配置 Timidity++

安装 Timidity++ 之后,需要在它的配置文件里指定一下要使用的 SoundFont,具体来说是在配置文件里面新增一行形如 soundfont <SoundFont 文件路径> 的内容,在 Arch Linux 上配置文件位于 /etc/timidity/timidity.cfg,关于配置文件更多操作参见 timidity.cfg(5)

# /etc/timidity/timidity.cfg 
# 使用 FluidR3 SoundFont 
soundfont /usr/share/soundfonts/FluidR3_GM.sf2

整体使用流程

比如说,要编写这么一小节动词大词的乐谱,有图片,有声音(后文音频下方的链接皆为对应的 LilyPond 原文件):

simple-rhythm

simple-rhythm.ly

对应的 LilyPond 文件可以这样写(个人总结的比较方便的写法):

\version "2.22.1"
 
main = {
  \drums {
    \tempo 4 = 100
    \numericTimeSignature
    \time 4/4
 
    <<
      {
        hh8 hh8 <<hh8 sn8>> hh8 hh8 hh8 <<hh8 sn8>> hh8
      }\\{
        bd4 r4 bd4 r4
      }
    >>
  }
}
 
\score {
  \main
  \layout {}
}
 
\score {
  % https://lilypond.org/doc/v2.22/Documentation/notation/using-repeats-with-midi
  \unfoldRepeats {
    \main
  }
  \midi {}
}

然后将上述内容存为名为 simple-rhythm.ly 的文件(以 .ly 结尾即可,这里名称只是示例)。

启动任意终端模拟器,将 shell 的工作目录切到 simple-rhythm.ly 所在的位置,执行以下可以得到排版好的 SVG 格式的乐谱和对应的 MIDI 文件:

lilypond -fsvg -dcrop simple-rhythm.ly

会得到:

接下来再使用 timidity 处理上一步得到的 MIDI 文件,以及通过 ffmpeg 转换音频格式,就可以得到方便传输的 m4a 格式的音频:

timidity simple-rhythm.midi  -Ow -o - | ffmpeg -i - simple-rhythm.m4a

会得到 simple-rhythm.m4a,即为乐谱对应的音频。

看起来有点长,不要慌,比真的去打动词大词简单多了。

记录音符

首先,通过对照乐谱的图片和上面的内容,应该可以脑补到其中这段是记录音符的部分:

<<
  {
    hh8 hh8 <<hh8 sn8>> hh8 hh8 hh8 <<hh8 sn8>> hh8
  }\\{
    bd4 r4 bd4 r4
  }
>>

这里的每一个单词+数字组成的内容就是一个确定时值的音符,比如 hh8,表示八分音符长度的一次(闭合状态的)踩镲的敲击,数字前面的单词表示音符,数字本身表示时值,比如 4 就表示四分音符,以及再打一个小数点的话变成 4. 的话,就是附点四分音符。然后 hh 也可以写成 hihat,前面的音符可以写成 hihat8,表示的是完全相同的意思,只是简写和全写的关系,完整的简写/全写对照、以及更多的音色对应的记法可以参照LilyPond — Notation Reference v2.22.1: A.15 Percussion notes。本文后面也会列出一些常用的音符。

上面用到的剩下的音符分别是:

  • bd/bassdrum:底鼓
  • sn/snare:军鼓
  • r:休止符

知道音符含义之后,这个时候我们来看一下 <<hh8 sn8>>,这里面有两个不同的八分音符,然后回头听上面的动词大词,第三个和第七个八分音符的位置同时有踩镲和军鼓的声音,这个尖括号括起来就是表示里面的音符是在同一个时刻上的。

再把视野放大一点,看到包裹所有音符的那一大块符号:

<<
  {
    % 手上的音符
  }\\{
    % 脚上的音符
  }
>>

这是一种记录两个声部内容的写法,两对 {} 里的内容分别对应乐谱的第一和第二声部,然后默认情况下两个声部的符杆方向是不一样的,把想要朝上的音符写在第一声部,朝下写第二声部,就达成了前面定义的鼓谱的音符的布局。这并不是唯一的控制音符朝向的方法,也不是唯一的描述多个声部的方法,只是写起来比较简单一点就这样了。另一方面,整个乐谱里面,可以有任意数量、先后出现的 <<{}>>\\<<{}>> 这样的记号,里面两个声部占的时值是相同的,实践上把一段节奏型的一个或者几个小节、一段 Fill in 的多个小节写成一块会比较方便,如果一块 <<{}>>\\<<{}>> 里的小节太多了的话,写到后面会比较难分辨两个声部分别写到哪儿了,怎么对应的。

音符时值与名字的简写

除了音符的名字有简写的别名之外,还有一些常用的简写重复内容的方式。

对于一串时值不同的同种音符,可以省掉第一个之后的音符的名字,只写表示时值的数字,比如:

shorthand-1

shorthand-1.ly

sn8 sn16 sn16 sn16 sn16 sn8
% 可以简写成
sn8 16 16 16 16 8

对于一串时值相同的不同音符,可以省掉第一个之后音符的音符的时值,只写表示音符的标识,比如:

shorthand-2

shorthand-2.ly

sn16 tomh16 toml16 sn16 toml16 sn16 tommh16 sn16
% 可以简写成
sn16 tomh toml sn toml sn tommh sn

简写虽然写起来少敲几下键盘,但是阅读起来不一定总是方便的,所以看情况用吧。

音符上的文字标注

比较常见的情景是标记左右手,在音符的末尾跟上 ^"TEXT HERE" 就可以在音符上方加文字,如果把 ^ 换成 _ 的话,文字就会出现在音符下方。

text

text.ly

sn4^"L" 4^"R" 4_"L" 4_"R"
16^"L" 16^"R" 8^"L" 8^"L" 16^"L" 16^"R"
16_"L" 8_"R" 16_"R" r16 sn16_"R" 16_"L" 16_"R"

更多形式的文字和样式设定参见 LilyPond — Notation Reference v2.22.1: 1.8.1 Writing text

连音(Tuplet)

连音可以用来表示一些不能用简单时值记号表示的伤害大脑的时值。比如说,把一个四分音符的时值分成三等分,重新分给一长一短的两个或者一样长度的三个音符这样的操作。

以一个四分音符分长度里演奏均匀的三个音符的,三连音八分音符来说,表示的方式是 \tuplet 3/2 {sn8 sn8 sn8}{} 内的音符是要塞到一组连音里面的音符,3/2 表示 {} 里的音符的时值变成了原来塞2个音符的时间长度里要塞3个同样的音符才能填满,或者说 {} 里一个音符的时值变成原来长度的 2/3,结果就是三连音八分音符看起来是 3 个八分音符,实际占的是 2 的正常的八分音符的时间长度。另外在表示时值变化的分数后面可以多写一个数字,表示连音要以几分音符的长度进行分组显示,在写一长串同类连音的时候比较方便。

triplets

triplets.ly

sn8 8 8 8 \tuplet 3/2 4 {8 8 8 8 8 8}

更多细节参见 LilyPond — Notation Reference v2.22.1: 1.2.1 Writing rhythms#Tuplets

连接线(Tie)

这里指连接两个同种音符的弧线,在两个要连线的音符的第一个音符末尾加一个 ~ 即可,比如 bd8~ bd8

tie

tie.ly

<<
  {
    hh8 8 <<hh8 sn8>> hh8 8 8 <<hh8 sn8>> cymc8~
    cymc8 hh8 <<hh8 sn8>> hh8 8 8<<hh8 sn8>> hh8
  }\\{
    bd4 r4 bd8 8 r8 bd8~
    bd8 bd8 r4 bd8 8 r4
  }
>>

更多细节参见LilyPond — Notation Reference v2.22.1: 1.2.1 Writing rhythms#Ties

即兴记号(Improvisation)

即兴记号用来表示一段主体节奏确定,具体怎么演奏随意发挥的内容。这个功能是由 \improvisationOn\improvisationOff 两个命令组成的,这两个命令之间的音符会变成即兴记号(一条斜线),音符的种类会影响斜线在五线谱的上下位置,以及输出音频的时候,即兴部分会用指定的音符的音色(虽然显示上是一条斜线),一般都用同一种音符就行了,毕竟只是为了表示节奏,用 sn 起始画出来差不多在中间的位置,应该有更细节的控制样式的操作,这里就不细说了。

improvisation

improvisation.ly

<<
  {
    hh 8 8 8 8
    \improvisationOn
    sn4. 8~
    4. 8~ 4. 8
    \improvisationOff
  }\\{}
>>

更多细节参见LilyPond — Notation Reference v2.22.1: 1.1.4 Note heads#Improvisation

重音/强调(Accent)

在普通音符后面写上->即为重音,比如普通的敲军鼓 sn8,对应重音的话写成 sn8->

更多细节参见LilyPond — Notation Reference v2.22.1: 1.3.1 Expressive marks attached to notes#Articulations and ornamentations

鬼音(Ghost note)

在音符前面加上 \parenthesize 就会把原来的音符变成鬼音,比如 sn8 对应的鬼音的话写成 \parenthesize sn8

更多细节参见LilyPond — Notation Reference v2.22.1: 2.5.1 Common notation for percussion#Ghost notes

常用的音符

乐器/击打方式/音色 LilyPond 的写法 说明
底鼓(Bsss drum) bd, bassdrum 普通一击
一嗵鼓 tomh, hightom 适用于三个嗵鼓的情况,其它数量的有别的嗵鼓音符可以用。
二嗵鼓 tommh, himidtom 适用于三个嗵鼓。
落地嗵鼓 toml, lowtom 适用于三个嗵鼓。
军鼓(Snare drum) sn, snare 普通一击
边击(Rimshot) 同时击中鼓面和鼓框的认真一击
没有找到直接的音符,但是可以手动用 LilyPond 指令画出来
退一步可以用写成军鼓的重音 sn->
制音边击(Cross Stick/Side Stick/Rim Click) ss, sidestick 槌头放鼓面上,用鼓槌尾部敲鼓框,类似敲木头的声音
鬼音(Ghost note) \parenthesize sn 非常轻的一击,几乎听不到,但是有一点感觉的声音
Stick Shot 一只鼓槌槌头放在鼓面,用另一只鼓槌敲鼓槌的中间部分
没有找到直接的音符,可能也是要手画。
重击镲 cymc, crashcymbal
节奏镲 ride, ridecymbal 普通敲击镲面
节奏镲(镲帽) rb, ridebell 敲节奏镲的镲帽
踩镲 hh, hihat 闭合状态踩镲的敲击
踩镲(闭合) hhc, closedhihat 闭合状态踩镲的敲击
一般用 hh 可以替代,这个主要是更明显地标示踩镲的闭合
踩镲(全开) hho, openhihat 张开状态踩镲的敲击
踩镲(半开) hhho, halfopenhihat 半开状态的踩镲的敲击
踩镲(脚踏) hhp, pedalhihat 脚踏控制的一次踩镲开-合,让两个镲片碰撞产生的声音
踩镲(Foot Splash/Hihat Splash) 脚踏控制的一次踩镲的开-合-开,让镲片碰撞产生声音并且延续
没有直接能用的音符,一些地方记录成 hhp 的符号再多加一个圆圈表示

更多参照LilyPond — Notation Reference v2.22.1: A.15 Percussion notes

自定义音符的样式和位置可以从这里开始:LilyPond — Notation Reference v2.22.1: 2.5.1 Common notation for percussion#Custom percussion staves

乐谱的总体设定

一个乐谱除了音符之外,还需要一些额外的信息才能变得完整,比如拍号、速度之类的,这就是把音符括起来的外面一圈内容表示的信息:

\drums {
  \tempo 4 = 100
  \numericTimeSignature
  \time 4/4
 
  % 乐谱的音符...
}

\time 4/4 比较直观,是指每小节四拍,四分音符一拍的拍号,它表示随后的音符都是按照这个拍号来的,如果乐谱到中间要改变拍号了,就再写一个 \time 指定新的拍号就好了。更多用法参见 LilyPond — Notation Reference v2.22.1: 1.2.3 Displaying rhythms#Time signature

\numericTimeSignature 会让其后出现的拍号显示成数字的形式,默认情况下 LilyPond 会将 4/4 拍显示成一个大 C 记号,这是另外一种流派的拍号表示,可以根据个人需要选择拍号的表示。

\tempo 4 = 100 表示了乐谱的速度,4 = 100 即为每分钟 100 个四分音符。除了用固定的数字表示之外,速度也可以定成一个范围,比如 \tempo 4 = 100 - 120,或者用文本的方式表示,例如 \tempo "Allegretto",或者你不想做选择题,数字文字全都要也是可以的,不给乐谱写 \tempo 标记速度也可以,都可以,参见 LilyPond — Notation Reference v2.22.1: 1.2.3 Displaying rhythms#Metronome marks

\drums {} 表示记录一段鼓谱(准确来说是打击乐谱),只是写鼓谱的话不用太关心它背后具体的功能,好奇的话可以参见 LilyPond — Notation Reference v2.22.1: 2.5.1 Common notation for percussion#Basic percussion notation

另外,以 % 开头的一行内容是注释,那一行的内容不会被 LilyPond 处理。

乐谱的输出设定

虽然已经能表示鼓谱,能写音符了,还需要一些操作让 LilyPond 知道总共有哪些乐谱,需要怎么被输出,所以有了剩下的一块内容:

\version "2.22.1"
 
main = {
  \drums {
    % 乐谱内容
  }
}
 
\score {
  \main
  \layout {}
}
 
\score {
  % https://lilypond.org/doc/v2.22/Documentation/notation/using-repeats-with-midi
  \unfoldRepeats {
    \main
  }
  \midi {}
}

变量的使用

首先是离乐谱最近的 main = {},这是 LilyPond 的定义变量的功能,随后的地方可以用 \main 这个写法来引用 main = {} 里面包含的乐谱,main 这个名字只是顺手取的,可以根据实际内容取合适的名字,可以是一整个乐谱(示例的使用场景),也可以是一小段音符,比如:

(注意:鼓谱的音符需要用 \drummode 包起来)

dadada = { \drummode { sn8 8 8 8 8 8 8 8 } }
zizizi = { \drummode { hh8 8 8 8 8 8 8 8 } }
 
main = {
  \drums {
    % 引用上面的两段内容
    \dadada \zizizi
    \zizizi \dadada
  }
}

更多关于变量的使用介绍参见 LilyPond — Learning Manual v2.22.1: 2.4.1 Organizing pieces with variables#Basic percussion notation

排版与 MIDI 输出

\score {} 是 LilyPond 文件的基本组成单位,所有的表示音乐的内容都需要放到某个 \score 里面。\score 内部的 \layout 表示为当前 \score {} 内的音乐排版一份可打印的输出,而 \midi 是表示为 \score {} 里的音乐创建一份 MIDI 输出。

理论上一个乐谱只需要一个 \score {},直接写成:

\score {
  {
    % 令人心动的音乐
  }
  \layout
  \midi
}

就可以表示同时输出可打印的输出和 MIDI 输出了。但是,由于 LilyPond 有表示乐谱的各种形式的重复的记号,它们不能被直接转换成 MIDI 信号,如果不加处理的话,重复记号部分的音符会在最后生成的音频里没有声音。可是重复记号是乐谱不可分割的一个基本功能,用还是得用的,于是 LilyPond 给出的方案是把乐谱分成两份,一份普通地用 \layout 产生一份可打印的输出,另一份,先把重复记号都展开成实际的音符(\unfoldRepeats {} 所做的事情),然后再产生 MIDI 输出,结果就变成了写两个 \score {} 的样子。

更多关于 MIDI 输出与重复记号问题的说明参见 LilyPond — Notation Reference v2.22.1: 3.5.6 Using repeats with MIDI

更多关于 LilyPond 的文件结构的说明参见 LilyPond — Learning Manual v2.22.1: 3.1.1 Introduction to the LilyPond file structure

另外,如果想给乐谱加上标题和署名之类的信息的话,参见:LilyPond — Notation Reference v2.22.1: 3.2.1 Creating titles headers and footers

最后,文件开头还有个 \version "2.22.1",这是标识这份 LilyPond 文件对应需要什么版本的 LilyPond 才能处理的,通常来说,在你遇到问题之前都不用关心它。如果使用 Frescobaldi 来编写 LilyPond 文件的话,它会自动为新文件创建一行合适的 \version

生成乐谱

乐谱都写好了,排版的事情就可以交给 LilyPond 了,在终端里使用 lilypond 命令就可以创建排版好的图片格式的乐谱,以及 MIDI:

lilypond -fsvg -dcrop "刚刚写好的 LilyPond 乐谱.ly"

关于参数上,常用的有 -f,后面跟上文件的格式,可以控制输出的格式,比如 pdfpng,打印的话当然是 pdf,网页上使用的话,比较适合用 SVG 格式,因为能获得不受缩放影响的清晰图像。(注:MIDI 格式是只要文件里写了 \midi 就会输出,不需要在这里额外加参数)。

-dcrop 并不是必须的,只有在需要输出一小段乐谱的图片的时候才会用到。因为 LilyPond 默认输出的是 A4 尺寸的内容,即使只有一个小节的内容也会用那么大的面积,像本文里用到的乐谱片段的图片的话,我只需要只包含音符一个小小的图片,就需要用这个参数了。

完整的参数说明参见 LilyPond — Usage v2.22.1: 1.2 Command-line usage

生成音频

前面提到了,timidity 可以用来从 LilyPond 产生的 MIDI 文件生成能听的音频,使用上像是这样:

timidity "刚刚写好的 LilyPond 乐谱.midi"  -Ow -o "刚刚写好的 LilyPond 乐谱.wav"

-Ow 参数会让 timidity 输出 WAV 格式的音频。-o 是指定输出要保存到的文件名字。

更多 timidity 的参数参见 timidity(1)

其实 WAV 格式已经是能够用常规的音频播放器播放的了,如果想得到别的格式的音频,可以用 ffmpeg 来转换一下,比如生成在这个页面里用的 m4a 格式的音频,就可以:

timidity "刚刚写好的 LilyPond 乐谱.midi"  -Ow -o - | ffmpeg -i - "刚刚写好的 LilyPond 乐谱.m4a"

November 06, 2021 12:00 AM

October 27, 2021

ヨイツの賢狼ホロ

带键盘的 Android 手机和多亲 F21 Pro 折腾记

因为人(?)老了就喜欢怀旧? 😂

对咱这个高不成低不就不知道算不算老人的家伙来说呢,能够拿来怀旧的应该会有那些“古老”的 手机游戏了。(也没有多么老啦,就是差不多十年前那些 Java 手机游戏了,好像叫啥 J2ME? 算了不管了后面就都叫 Java 了,希望大家不会和现在还在的那个 Java 搞混。)

虽然现在也有像是 J2ME-Loader 这种 Android 上也可以用的 Java 模拟器了,但是在触摸屏上按虚拟按键完全没有感觉嘛…… 所以咱也开始物色有键盘的 Android 手机了。

好啦咱知道去收购一些老的手机也是可行的解决方案,那下面的内容就完全没有必要写了。 是不是咱可以鸽了 (x)

举几个键盘 Android 手机的栗子

虽然 Android 的操作更多的是以触屏为主,但是也不是没有有物理键盘的机型啦。 比如摩托罗拉里程碑之类的,但是那个好像太古老了点……

最近几年最能拿出来举的例子大概就是黑莓了,例如最后一部亲自操刀的 Priv 和后面授权 TCL 设计生产的 KeyOne / Key2 。不过 TCL 也在 2020 年撒手不干了就是...

除了黑莓以外,也有几家公司设计过带物理键盘的 Android 手机,例如 Unhertz TitanF(x)tec

不过上面这些还是有一个微妙的问题,它们都是传统的 QWERTY 键盘布局,所以按键普遍的都比较小。 方向键也都在一些奇妙的地方或者干脆就没了。虽然大部分 Java 手机游戏都能用数字键代替方向键来着, 等等,这些手机上的数字键基本也要靠 Fn 之类的组合键触发出来啊……

以及它们的性价比也不怎么漂亮。(家境贫寒.jpg)

后来上网瞎逛的时候就看到了标题说的那个 多亲 F21 Pro 。 是常见的 12 键键盘布局,Helio A20 的性能虽然不强,但是满足咱这需求应该是够用的, 于是就买了一台 4G+64G 版本。

在咱开始写这篇文章的时候,才发现还有一个 F21 Pro+,平台换成了紫光展锐 T310。 同时可以选择学生版和标准版(标准版可以自由安装应用,学生版不行,F21 Pro 也不行), 不过只有 3G+32G 版本可选,如果汝不想像后面那样如此麻烦的话,去买那个就行。 3+32 应该也足够用了,除非汝往里面装了几万个游戏那样的(笑)。

借应用市场刀装第三方应用

既然这都拿“学生手机”作为卖点了嘛,那自然不能自己安装应用就是设计使然而不是 Bug 咯。 这个系统貌似是魔改了系统的软件包安装程序,如果包名不在白名单里就会提示 “禁止安装第三方应用,请去应用市场安装”。 多亲似乎也从 2 Pro 那时学来了不少的反制措施, 例如封掉了 adb 安装,提前占了设备管理员应用的位置之类的。

不过后来有人发现系统自带的应用市场其实是能安装其它的应用的,只要把正在下载中的安装包换成汝想要的软件就行了。 就算后来的系统更新封堵了一阵子,也又被发现只要把旧版的应用市场装上就能绕过了……

也有人写出了能半自动操作的工具了,例如 MlgmXyysd/k61v1injector 。 但是咱比较懒(上面那个依赖 PHP),所以就手动操作一把了。

  • 在 F21 Pro 上启动开发者选项里的 USB 调试。和普通的 Android 手机一样的方法咱就不用再啰嗦了吧?
  • 接上电脑,在手机上授权电脑,然后打开一个终端。
  • 清除手机上应用市场的数据。(这是为了接下来偷懒方便(x))
  • 打开手机的应用市场,下载一个软件。考虑到下载的时间因素,建议选一个稍微大一点的软件, 或者限制一下手机的网速也是可行的。

然后在终端上输入(汝应该知道要把哪里换成汝自己要安装的 APK 文件的路径,对吧?):

# $() 可以把括号内的命令的结果替换到上层的输入里。
# 例如这里的 adb shell ls /sdcard/Android/data/com.duoqin.promarket/files/Download/
# 会列出这个目录(应用市场的下载目录)里的文件列表。
# 如果汝刚刚是清除了应用数据再下载的话,这里就只有汝当前正在下载的应用了。
# 于是瞒天过海大法发动(x)

$ adb push /path/to/your/apk \
/sdcard/Android/data/com.duoqin.promarket/files/Download/$(adb shell ls /sdcard/Android/data/com.duoqin.promarket/files/Download/)

不过这种方法唯一的缺陷就是,应用更新以后汝还是要这么来一遍,所以还是要研究一下怎么把这破烂限制解掉。 奈何都喜欢藏着掖着……

安装第三方应用演示

效果大概就是这个样子。于是汝也可以把刚刚咱说的那个 Java 模拟器装上,再找来几个老游戏的 JAR。 就可以愉快的玩耍啦。

f21pro_j2meloader

因为 F21 Pro 的屏幕分辨率是 480x640 ,正好是以前最常见的 240x320 的两倍。 所以在模拟器里设置好全屏显示,再隐藏掉虚拟键盘以后的感觉就和以前差不多了。 至于按键手感嘛……这个只能说因人而异了。

f21pro_play

其它的小吐槽

  • 如今很多 App 都不会给键盘做优化了啊,还好 F21 Pro 有触摸屏,用起来不会像之前流行 的 C558 那样麻烦。
  • 以及该卡的国产“小而美”还是卡,刚装完能吃掉接近 1G 储存空间可还行……
  • fastboot flashing unlock 对这个是可用的,但是这个手机没有音量键没法确认啊 😂

以及这里面手机的截图其实是截取的电脑用 scrcpy 连接到手机的画面,就这样吧。

by ホロ at October 27, 2021 04:00 PM

October 04, 2021

Lainme

在CentOS 7上安装百度云盘

在CentOS 7上安装百度云盘提供的RPM后会发现软件无法打开,原因是GCC的版本过低。解决方法和Skype类似,先安装一个较高版本的GCC(6.1.0以上),然后在启动百度云盘之前将它的lib目录增加到LD_LIBRARY_PATH中。

比如高版本的GCC安装到了/usr/local,那么可以将/usr/share/applications/baidunetdisk.desktop修改成如下形式

[Desktop Entry]
Name=baidunetdisk
Exec=/usr/bin/bash -c "export LD_LIBRARY_PATH=/usr/local/lib64:$LD_LIBRARY_PATH && /opt/baidunetdisk/baidunetdisk --no-sandbox %U"
Terminal=false
Type=Application
Icon=baidunetdisk
StartupWMClass=baidunetdisk
Comment=百度网盘
MimeType=x-scheme-handler/baiduyunguanjia;
Categories=Network;

by lainme (lainme@undisclosed.example.com) at October 04, 2021 10:23 PM

October 01, 2021

百合仙子

纯 CSS 实现倒三角箭头

本文来自依云's Blog,转载请注明。

想实现这样的悬停提示框效果:

悬停提示框示例

这个绝对定位的框不是问题,边框的阴影也不是问题。问题是,我怎么弄出来那个倒三角的箭头呢?

在网上搜了一圈,找到的代码是这样的:

.tooltip .tooltiptext::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -5px;
  border-width: 5px;
  border-style: solid;
  border-color: black transparent transparent transparent;
}

拿边框给挤出来的。倒三角是有了,但是是纯色的,像这样子:

纯色背景提示框

很多 tooltip 提示框都是这么实现的,反正它们是黑底白字,并没有个边框。而我的是白底灰框,在下边接个纯色的三角块,就太丑啦。可是我又不想在这个被挤的边框上玩出花来,就只能是纯色的一块,所以此路不通了。

于是我就想啊,这个三角箭头怎么能弄出来呢?它不就是个正方形的框被砍了一半,再旋转45°吗?正好 CSS 能单独控制每一边的边框。也不用挤边框那么难以理解的操作了。于是:

#reply-popup::after {
  content: "";
  position: absolute;
  top: calc(100% - 6px);
  left: calc(50% - 6px);
  width: 10px;
  height: 10px;
  background-color: white;
  border-width: 1px;
  border-style: solid;
  border-color: transparent #bfbfbf #bfbfbf transparent;
  transform: rotate(45deg);
}

效果还不错,除了没有阴影,很不搭。那就把阴影加上?

  box-shadow: 0 0 5px gray;

结果嘛,不愧是叫 box-shadow,这阴影真就是个 box,不管你的边框,每一边都有的。我尝试调整 z-index 想让提示框把「无边阴影」给遮挡住,但是没成功。

于是又想办法。我记得 CSS 有个 clip 的功能来着?后来发现现在有个 clip-path,支持多边形,挺好的,我可以弄个三角形给剪裁一下,不用求助了行内嵌 SVG 了。

我尝试了一下,clip-path 是会随着 transform 一起旋转的,这似乎让火狐的 clip-path 多边形编辑功能很困惑,实际效果和显示的控制点之间我没能看出什么关联来。于是放弃可视化编辑,还是老老实实地算坐标点。也不复杂啦,右上、右下、左下,三个点就好了。然后放大两倍来容纳阴影。这样会正好切一半,在 Google Chrome 上没啥问题,但是在火狐上,不会完全遮挡住悬停元素的框,会漏出那么一丝丝出来。往上移一像素又太多,所以我把三角形底边上的两个端点稍微移动了一下,来挡住这一丝丝边框。最终规则是这样的:

  clip-path: polygon(145% -50%, 150% 150%, -50% 145%);

PS: 大家一直说 Google Chrome 更适合开发,但一些细节上火狐还是做得更多。比如火狐虽然没有角度编辑器,但是有多边形编辑器。火狐会在 DOM 树上把伪元素显示出来方便查看。火狐也会给元素标注事件、滚动、溢出、弹性盒,但 Google Chrome 只标注了弹性盒。话说 Google Chrome 最近终于加上了中文翻译了呢。另外 Google Chrome 的字体选择真不听话,难怪大家都喜欢强制指定一大串字体名。

哦对了,火狐可以用右键点击元素,然后按 q 键来检查元素。Google Chrome 没这个快捷键,得肉眼扫右键菜单。不知道有没有什么更方便的办法。

by 依云 at October 01, 2021 10:38 AM

September 26, 2021

Lainme

在CentOS上安装UOS版微信

UPDATE: UOS微信又升级了,Freechat不能用了,wechat-uos的脚本也不能用了。需要的可以参照最新的AUR安装,但需要系统的gcc在4.9以上,glibc在2.25以上。因为这两个条件CentOS 7都没有,要解决起来很麻烦,我就不更新了,已放弃在电脑上使用微信……

UPDATE: Freechat的体验更好,地址 https://github.com/eNkru/freechat

照着AUR上的wechat-uos包安装就可以了: https://aur.archlinux.org/packages/wechat-uos/

我稍微改了改,装到$HOME/software/wechat-uos下,然后用stow管理,脚本如下

wechat-install.sh
#!/bin/bash
 
# Meta definition
PKGNAME=wechat-uos
PKGVERS=2.0.0-2
DEBNAME=com.qq.weixin_${PKGVERS}_amd64.deb
PKGLINK=https://cdn-package-store6.deepin.com/appstore/pool/appstore/c/com.qq.weixin/${DEBNAME}
WORKDIR=/tmp/${PKGNAME}
DESTDIR=$HOME/software/${PKGNAME}
SHARE_CORE_PATH=${DESTDIR}/share/${PKGNAME}
SHARE_CRAP_PATH=${SHARE_CORE_PATH}/crap
SHARE_ICON_PATH=${DESTDIR}/share/icons
SHARE_APPS_PATH=${DESTDIR}/share/applications
SHARE_APPS_FILE=${DESTDIR}/share/applications/${PKGNAME}.desktop
BINARY_PATH=${DESTDIR}/bin
BINARY_FILE=${BINARY_PATH}/${PKGNAME}
 
# Working directory
sudo rm -rf ${WORKDIR}
mkdir -p ${WORKDIR}
cd ${WORKDIR}
 
# Install depends
sudo yum install bubblewrap bsdtar ImageMagick
 
# Download files
wget ${PKGLINK}
wget https://aur.archlinux.org/cgit/aur.git/plain/uos-lsb?h=wechat-uos -O uos-lsb
wget https://aur.archlinux.org/cgit/aur.git/plain/uos-lsblk?h=wechat-uos -O uos-lsblk
wget https://aur.archlinux.org/cgit/aur.git/plain/uos-release?h=wechat-uos -O uos-release
 
# Processing and install the debian package
bsdtar -xf ${DEBNAME}
bsdtar -xf data.tar.xz
for s in 128 64 48 16; do
    NEWSIZE="${s}x${s}"
    convert -geometry ${NEWSIZE} \
        ${WORKDIR}/opt/apps/com.qq.weixin/entries/icons/hicolor/256x256/apps/wechat.png \
        ${WORKDIR}/opt/apps/com.qq.weixin/entries/icons/hicolor/${NEWSIZE}/apps/wechat.png
done
mkdir -p ${SHARE_CORE_PATH}
cp -a ${WORKDIR}/opt/apps/com.qq.weixin/files/* ${SHARE_CORE_PATH}/
mkdir -p ${SHARE_ICON_PATH}
cp -a ${WORKDIR}/opt/apps/com.qq.weixin/entries/icons/* ${SHARE_ICON_PATH}/
 
# Hacks
sudo mkdir -p /usr/lib/license
sudo install -Dm644 ${WORKDIR}/usr/lib/license/libuosdevicea.so -t /usr/lib/license/
sudo touch /etc/lsb-release
mkdir -p ${SHARE_CRAP_PATH}
install -Dm644 uos-lsb -t ${SHARE_CRAP_PATH}/
install -Dm644 uos-release -t ${SHARE_CRAP_PATH}/
install -Dm755 uos-lsblk -t ${SHARE_CRAP_PATH}/
 
# Binary
mkdir -p ${BINARY_PATH}
touch ${BINARY_FILE}
chmod +x ${BINARY_FILE}
echo "#!/bin/bash -e" > ${BINARY_FILE}
echo "bwrap --dev-bind / / \\" >> ${BINARY_FILE}
echo "  --bind ${SHARE_CRAP_PATH}/uos-release /etc/os-release \\" >> ${BINARY_FILE}
echo "  --bind ${SHARE_CRAP_PATH}/uos-lsb /etc/lsb-release \\" >> ${BINARY_FILE}
echo "  --bind ${SHARE_CRAP_PATH}/uos-lsblk /usr/bin/lsblk \\" >> ${BINARY_FILE}
echo "  ${SHARE_CORE_PATH}/wechat" >> ${BINARY_FILE}
 
# Desktop entry
mkdir -p ${SHARE_APPS_PATH}
echo "[Desktop Entry]" > ${SHARE_APPS_FILE}
echo "Name=WeChat" >> ${SHARE_APPS_FILE}
echo "Exec=${BINARY_FILE} %U" >> ${SHARE_APPS_FILE}
echo "Icon=wechat" >> ${SHARE_APPS_FILE}
echo "Type=Application" >> ${SHARE_APPS_FILE}
echo "StartupNotify=true" >> ${SHARE_APPS_FILE}
echo "Categories=Application;chat;" >> ${SHARE_APPS_FILE}
 
# Stow it
cd ${DESTDIR}/../
stow ${PKGNAME}

by lainme (lainme@undisclosed.example.com) at September 26, 2021 12:57 PM

September 19, 2021

Alynx Zhou

通过 USB HID over AOAv2 在 scrcpy 里模拟真实键盘

这篇文章同时有 中文版本英文版本

This post is both available in Chinese version and English version.

中文版本

三年前(2018 年)我在 scrcpy 的 GitHub 仓库里提了 这个 issue,因为我当时发现这个项目能把手机投屏到电脑上,也就是说我就可以在 Linux 下面通过 Android 手机聊 QQ 了(我当时大概也许还有高强度聊 QQ 的需求),不过试了之后发现很难用,因为它和直接在手机上插键盘不一样,显示的还是软键盘,虽然能通过电脑键盘触发输入法,但是却不能用数字键选词。我当时也不太懂,于是就发 issue 问开发者,@rom1v 回复说 Android 有个叫 HID over AOA 的协议可以实现,但是对有些设备来说有 bug,同时也需要有人花时间读 USB 规范然后做把 SDL event 转换成 HID event 的工作。我又想那能不能直接用电脑的输入法生成字符然后传给手机,@rom1v 表示现在就是这么做的,但是 Android 相关的 API 限制只能发送 ASCII 的字符,所以也行不通。

我当时稍微了解了一下这些相关的东西,不过显然超出了我的能力范围,于是这件事我就搁置了。一直到最近或者准确的说就是上周和 Hackghost 跟我提起说苹果似乎打算做手机投屏到电脑的功能,然后又说华为好像有个现成的。不过我对这些一向是漠不关心的,这些厂商就是又懒又坏的典型,不会做一个 Linux 版的客户端的。如果他们自己做不了或者不打算做,那就应该公开一点,让能做的人来做而不是藏着掖着。然后就是我们讨论了一气关于成本到底谁付了谁亏了的问题,我坚持认为厂商赚得已经够多了,成本和利润 Linux 用户在买手机的时候也是照样付的,只是这些人贪得无厌能少付出成本就少付出一点而已。最后我说已经有能做的人做了 scrcpy 这个项目出来,投屏完全没问题,支持各种平台,美中不足就是输入体验不太好。这时候我又想已经过去很久了,不如我再试试去看看能不能解决输入体验的问题,于是就回去翻了这个 issue。

运气不错,翻过去看到在 2019 年年初的时候 @amosbird 已经写了一份代码,他自称是能用的,然而不知道为什么当初没有合并进主线,现在多半也是跑不起来。我一开始想我把他这个在当前的 HEAD 上重构一下就好了,于是开始读他的代码。一开始还是没什么头绪,看起来他似乎写了不止一个功能,但是都在一个 commit 里面,又没什么关于思路的注释。随后我加上了他的 Telegram,不过他本人表示时间有点久了他也不记得自己写的代码都是什么意思(笑)。于是我只能硬着头皮啃了,好在我现在的经验比以前涨了很多,然后再同时啃 USB 和 Android 的协议,配合一些搜到的其他资料,最终了解了大概是怎么回事。最开始本来以为简单地合并一下代码就可以了,没想到还发现了他代码里的错误,基本是变成重写了。花了头两天事件让程序跑起来,然后用了几天调整成和现有代码一致的风格以便合并进去。 总之很是有一点新手村出来遇到 BOSS 暂时撤退,升级打怪三年之后杀回来的感觉。

做好之后的效果基本就是下面两张截图,Gboard 开始工作在外接硬件键盘的模式了:

1.png 2.png

既然是 USB HID over AOAv2,那很显然需要知道 USB HID 是怎么回事,USB 官方有一个 很长的 PDF 规定怎么成为一个合法的 HID 设备,说实话,看不太下去。如果你想要查一些有帮助的例子,直接查 AOAv2 多半是没戏的,只有 Android 自己一个惜字如金的文档页,我的经验就是你找那些主题是用单片机模拟键盘鼠标的文章,他们的目标和这个基本是一致的。不过我说好不碰硬件的话看来是算作废了。

基本上成为一个 USB HID 设备需要你发送一大堆的描述符到主机,不过我们这里有点不一样,因为 Android 设备连接电脑的时候,Android 是 USB 从设备,电脑是主设备,而 AOAv2 是从主机反向发数据到从设备,它不要求我们发送一大堆 USB 的描述符,只要向 Android 注册一个设备,发送 HID 的报告描述符,再发送 HID event,再注销就好了。这部分可以通过 libusb 这个库来实现 USB 的数据包传输,然后把 Android 的几个命令封装成函数就可以了,基本是在 https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/aoa_hid.c#L155-L246 这部分。

基础的 API 有了之后则是具体的发什么数据包了,HID 的数据实际上就是由 byte 组成的 buffer,首先就是报告描述符,这个描述符是让主设备知道每个发过来的 event 里面的每个 byte 都是什么含义,键盘的描述符其实相对是比较固定的,在 Device Class Definition for Human Interface Devices 这个 PDF 里面其实给了一个最简单的 USB 键盘的例子,这个也是保证在 BIOS 里面能正常使用 USB 键盘的最小集合,是在 Appendix B: Boot Interface Descriptors 下面的 B.1 Protocol 1 (Keyboard) 和 Appendix E: Example USB Descriptors for HID Class Devices 下面的 E.6 Report Descriptor (Keyboard)。不过有时候光知道这些还不够,比如报告描述符里面大部分都是两个 byte 一句话,第一个 byte 表示的是类别而第二个表示的是具体的值,后面的大概很好理解,但是第一个 byte 是怎么算出来的可能需要了解,这需要看那个 PDF 里 8. Report Protocol 这一节了,或者中文的话可以看 这篇知乎文章,然后你就会明白为什么有时候看起来数字不一样结果含义却一样了,因为其实第一个 byte 的每个 bit 都是有分别的含义的。然后就是对于 Usage Tag 这个有很多,被放在 另一个单独的 PDF 里面了。

我还是把 https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L28-L144 这段代码贴过来好了,详细的说明我加在了注释里面:

unsigned char kb_report_desc_buffer[]  = {
    // Usage Page (Generic Desktop)
    // 键盘这个字段应该是 Generic Desktop,为啥是这个我也不知道。
    0x05, 0x01,
    // Usage (Keyboard)
    // 也不用我解释了吧,自己查表去。
    0x09, 0x06,

    // Collection (Application)
    // 然后基本上 USB HID 描述符要有不同的 Collection,
    // 代表你发到主机的一组数据,
    // 一个设备最少有一个 Collection 吧,不然也没意义了。
    // 为啥是 Application 好像是因为 Keyboard 的 Usage Page tag 在这个里面。
    0xA1, 0x01,
    // Report ID (1)
    // 你会发现最基础的那个键盘示例里面没有这个字段,
    // 实际上当你只发一种数据的时候这个字段可以省略。
    // 但是现在是个机械键盘都带媒体控制按键吧,那个要算另一个 Collection 的,
    // 所以就通过 Report ID 区分,同时你发数据的时候第一个 byte 就得说明自己发的是哪个
    // Report ID。所以不要问为什么网上说 USB HID 键盘一个事件 8 个 byte 我们却发了 9 个。
    0x85, 0x01,

    // Usage Page (Keyboard)
    // 这里注意 Usage Page 和 Usage 不一样咯。好像这个文档里也叫 Key Codes 来着。
    0x05, 0x07,
    // Usage Minimum (224)
    // 一般来说一组报告数据包含很多用途,你不可能把所有的用途都列出来,给个用途范围就可以了。
    // 这里表示最小的用途 tag 是 0xE0,应该是 Left Control 的意思。
    0x19, 0xE0,
    // Usage Maximum (231)
    // 最大的用途 tag,是 Right GUI 键。这个 byte 的含义就是键盘上的八个修饰键。
    // Left Control,Right Control,Left Alt,Right Alt,Left Shift,Right Shift,
    // Left GUI,Right GUI。
    // 不过一定要记住 16 进制数转成 2 进制时候最右边是第 0 位,也就是说上边的顺序要倒过来。
    0x29, 0xE7,
    // Logical Minimum (0)
    // 每个 bit 的逻辑最小值当然是 0 啦。
    0x15, 0x00,
    // Logical Maximum (1)
    // 按下去就变成最大值 1。
    0x25, 0x01,
    // Report Size (1)
    // 每个数据只占 1 bit。
    0x75, 0x01,
    // Report Count (8)
    // 一共 8 个数据。
    0x95, 0x08,
    // Input (Data, Variable, Absolute): Modifier byte
    // 关于 Input tag 是什么意思太长了,自己查去吧,总之和上面那些要符合。
    0x81, 0x02,

    // 下面这段是厂商保留位,一般只要写一个为 0 的 byte 就可以了。
    // Report Size (8)
    0x75, 0x08,
    // Report Count (1)
    0x95, 0x01,
    // Input (Constant): Reserved byte
    0x81, 0x01,

    // 这部分表示主设备返回的关于 LED 灯亮的信号,
    // HID 键盘自己是不保存什么大写锁定的状态的。
    // 不过我们毕竟不是真的键盘,这部分抄一下就完事了,程序直接忽略。
    // 总之键盘上有 5 个灯,和上面的修饰键差不多,一个 bit 代表一个灯。
    // Usage Page (LEDs)
    0x05, 0x08,
    // Usage Minimum (1)
    0x19, 0x01,
    // Usage Maximum (5)
    0x29, 0x05,
    // Report Size (1)
    0x75, 0x01,
    // Report Count (5)
    0x95, 0x05,
    // Output (Data, Variable, Absolute): LED report
    0x91, 0x02,

    // 5 个 bit 对不齐,这 3 个 bit 是为了凑整的。
    // Report Size (3)
    0x75, 0x03,
    // Report Count (1)
    0x95, 0x01,
    // Output (Constant): LED report padding
    0x91, 0x01,

    // 下面就有意思了,键盘上有 101 个普通键,你肯定是不会同时按下 101 个键的,
    // 所以也不需要像修饰键那样每个 bit 代表一个键
    //(当然理论上你写个描述符说我这个键盘就是这样的也未尝不可啦),
    // 所以实际上这里返回的是一个数组,每个 byte 代表一个被按下去的键的键码。
    // Usage Page (Key Codes)
    0x05, 0x07,
    // Usage Minimum (0)
    // 用途最小值从不代表任何键的空值 0 开始。
    0x19, 0x00,
    // Usage Maximum (101)
    // 这里就是 101 啦,标准键盘上的按键数量,如果要支持非标准键盘自己查表去。
    0x29, HID_KEYBOARD_KEYS - 1,
    // Logical Minimum (0)
    // 因为每个 byte 放的都是键码,键码最小值是 0。
    0x15, 0x00,
    // Logical Maximum(101)
    // 最大值就是第 101 个键。
    0x25, HID_KEYBOARD_KEYS - 1,
    // Report Size (8)
    // 每个值都是 1 byte。
    0x75, 0x08,
    // Report Count (6)
    // 最基础的 USB 键盘一次最多能返回 6 个被同时按下的按键,
    // 你的 USB 键盘实际上很可能会告诉电脑它能报告更多,
    // 不过我们毕竟不是真的键盘,6 个键应该能满足大部分情况了。
    0x95, HID_KEYBOARD_MAX_KEYS,
    // Input (Data, Array): Keys
    // 这里也查表去吧,总之变成了 Array 而不是 Variable。
    // 一个 Collection 应该是只能有一个 Array,必须在末尾来着。
    0x81, 0x00,

    // End Collection
    // 这样我们代表正常键盘的 Collection 就结束了。
    0xC0,

    // Usage Page (Consumer)
    // 下面则是代表媒体控制的按键,他们属于另一个用途页了。
    0x05, 0x0C,
    // Usage (Consumer Control)
    // 这些键在另一个用途下面。
    0x09, 0x01,

    // Collection (Application)
    // 一个新的 Collection,数据和之前不一样了。
    0xA1, 0x01,
    // Report ID (2)
    // 一个新的 Report ID,
    // 所以这种 event 是 1 byte Report ID 和 1 byte 键码一共 2 byte。
    0x85, 0x02,

    // Usage Page (Consumer)
    // 下面的用途都算这里。
    0x05, 0x0C,
    // 这些和修饰键基本是一个意思,每个 bit 代表一个按键,不过这些用途不连贯了。
    // Usage (Scan Next Track)
    0x09, 0xB5,
    // Usage (Scan Previous Track)
    0x09, 0xB6,
    // Usage (Stop)
    0x09, 0xB7,
    // Usage (Eject)
    0x09, 0xB8,
    // Usage (Play/Pause)
    0x09, 0xCD,
    // Usage (Mute)
    0x09, 0xE2,
    // Usage (Volume Increment)
    0x09, 0xE9,
    // Usage (Volume Decrement)
    0x09, 0xEA,
    // 和修饰键差不多啦下面。
    // Logical Minimum (0)
    0x15, 0x00,
    // Logical Maximum (1)
    0x25, 0x01,
    // Report Size (1)
    0x75, 0x01,
    // Report Count (8)
    0x95, 0x08,
    // Input (Data, Array)
    0x81, 0x02,

    // End Collection
    0xC0
};

所以基本上我们构建的虚拟键盘就告诉主设备它是这么报告数据的。里面除了要注意具体的数据之外第一个 byte 要放 Report ID 之外也没什么。然后就是怎么把 SDL 的事件转换成 HID 事件了,一开始看 @amosbird 的代码,发现他把 HID event 理解成了和 SDL event 一样的东西了——有一个类似修饰键的数据和一个按下抬起的数据和一个具体哪个键的数据,用户按一个键就生成一个针对这个键单独的 event——但是其实不是,HID 键盘并不会直接告诉你哪个键按下还是抬起了,从上面的报告描述符也能看出来,实际上它是一个基于顺序的协议,比方说我按下了 C,给主机的事件可能是 C键 00 00 00 00 00,又按下了 B,再发一个 C键 B键 00 00 00 00,抬起来 C,再发一个 B键 00 00 00 00 00,然后主机对比之前和之后的事件,得到“C 抬起了”或者“B 按下了”的信息,这才是我们司空见惯的“键盘事件”。所以在程序里面我们需要做个反向操作,把单独针对某个按键的按下抬起的数据变成一个“有哪些键按下”的数据。这里其实很简单,我们内部用一个数组保存当前的按键状态,每次有 SDL 的按键事件发来就更新状态(不是所有按键事件都是那 101 个键哦),然后遍历这个数组,就可以利用哪些索引对应的值是 true 生成 HID 事件的内容了,同时 HID 也不要求发送的按键在数组里的顺序和按下的顺序一致,而且 SDL 的 scancode 和 HID 的值是一致的。对于修饰键更简单,SDL 每个事件里面的修饰键和 HID 一样,都是包含当前所有修饰键的状态,只是具体的哪个 bit 表示哪个键不一样,转换一下就可以了(https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L195-L223)。但是千万不要因为发来的按键不是那 101 个键就直接忽略掉整个事件,因为可能用户只是单独按了一下修饰键,所以如果要跳过只要跳过更新按键状态就好了https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L225-L269)。

可能有聪明的小朋友要问了,你这最多只能发六个键,我按下七个怎么办?自己好好读一下 HID 协议就行了,这种情况下需要回一个 phantom state,具体就是修饰键一切照常,六个 key 全返回 0x01https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L250-L259)。

基本上看到这里已经可以成功的给 Android 手机发 HID 键盘事件了,另外就是如果你用桌面环境的话,媒体按键应该会被桌面环境拦截,所以其实我没有发媒体事件,scrcpy 是用组合键单独处理了一部分功能。然后就是一些收尾,比如 @amosbird 忘记在程序结束之前取消注销 HID 设备了,这会导致如果你不拔掉 USB 线,触摸用的软键盘就一直出不来。

最后 @rom1v 表示 scrcpy 是在单独的线程里处理输入的,所以我也给 AOA 单独起了一个线程,这个只要照着 scrcpy 原本 controller 的线程抄一个就可以了。

你要是问为什么不把鼠标也用 HID 的方式模拟进去的话,我可以告诉你我也试过了,效果并不算好,AOA 是可以注册不止一个 HID 设备的,只要你分配不同的 ID 并作为参数发过去就可以了,这一部分代码我都留好了。但是一个主要的问题是 HID 鼠标只汇报 X 和 Y 的变化量,导致比方说我把鼠标从 SDL 的窗口挪走,MotionEvent 停止,HID 鼠标就会停在边框上,这时候我再从另一个方向挪进窗口,HID 鼠标的指针和你本机的指针就不再同步了,体验不如 scrcpy 自己注入事件的方式,所以我放弃了。

Windows 下面 libusb 似乎不能很好的连接 Android 手机发送数据,这个我没什么办法,看起来是个 陈年老 bug,考虑到我一开始的目的只是我自己能在 Linux 下面方便地聊 QQ,做成这样我觉得就可以了,我的代码逻辑正确,即使要修复,也不至于动 scrcpy 这部分而是动 libusb,@rom1v 也表示现在就很不错了。

当然因为依赖 USB,所以你利用 ADB over WiFi 的话就没办法用这个功能了,不过我用电脑时候一般会给手机充电,也无所谓。

提交的 PR 链接是 https://github.com/Genymobile/scrcpy/pull/2632/files,感觉是个大工程,最后实际上也就修改了一千行?不过确实挺难的。


English Version

(Title is Simulate Physical Keyboards in Scrcpy via USB HID over AOAv2.)

Three years (2018) ago I opened an issue on scrcpy's GitHub repo, because I just found using scrcpy I can mirror my Android phone to computer and control it, so I could use QQ on Linux via my Android phone (I still had to use QQ frequently at that time), but after I tried it, I found it's inconvenient, because it shows soft keyboard on Android phone instead of simulating physical keyboard, I can trigger input method via typing but using numbers to choose words is not available. I know little about the code at that time and ask the maintainer for some reason, @rom1v replied that there is a protocol called HID over AOA supported by Android can implement a physical keyboard, but there is a bug on some devices and needs some one take their time to read specifications and write some code to convert SDL event into HID event. Then I asked that how about using computer's IME and pass Chinese into Android, @rom1v said that's what scrcpy did but Android has a limitation that you can only pass ASCII chars.

I just tried reading some related things but it beyonds my ability, I just put it here. Until last week when I am talking with my friend Hackghost, he said Apple is making their own screen mirror implementation, and seems Huawei has one on their phone. I never care about Hardware manufacturer, neither Apple nor Huawei, because I think they are both lazy and evil, and they won't make a Linux client for their screen mirror implementation. If they are unable to make it, they should be open and allow users to do it by themselves. Then we talked about cost and profit, I insist that they have enough profit and Linux users also paid for their phone, so the feature should be available on Linux, but they are greedy and want more profit. Finally I said that there is already a hero who made scrcpy, and scrcpy runs on Windows, Linux and macOS, except for the bad IME experience. And I remebered it takes a long time after I opened the issue, maybe I should try again so I did it.

I am lucky that @amosbird already wrote some code in 2019 and he said it is able to work but he didn't send a PR, I guess it won't work now but I could refactor it to current HEAD so I start to read his code. At first I am wired that he implemented not only one feature in a single commit without any hinting about what he did. Then I managed to contact to him via Telegram, but he forget his code because 2 years past (lol). So I need to do it myself, I am more experienced that 3 years ago and I read USB and Android documents, with some other articles, I know what I should do. At first I think I just merge code, but finally I am re-writing because some mistakes in his code. It takes 2 days to make it work and some other days to tweak it. Sounds like that you met a BOSS when you are a newbie and you escaped, fight to level up and come back after 3 years.

Those are screenshots of this feature:

1.png 2.png

So it's USB HID over AOAv2, you need to know what's USB HID, there is a long PDF from USB IF telling you how to be a HID device, it's hard to read. And if you want examples, typing AOAv2 in Google is useless, you can only get a little page from Android's documents, I found that articles about simulating keyboard with single chip microcomputer (not sure whether this is the correct translation, some thing like Arduino or STM32) are helpful, however I've said that I won't touch hardware, my words are fake now.

Basically you need to send a lot of descriptors to host if you want to be a USB HID device, but things are different here. When you connect your Android phone to computer, your computer is the host and your Android is sub-device, AOAv2 is about sending data from host to sub-device, so we don't need most USB descriptors, just register a device to Android and send HID report descriptor, then send HID event, then unregister it. We can use libusb for data transfering and make some Android's command into functions, see https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/aoa_hid.c#L155-L246.

After API we need to decide what to send, HID just sends buffer made of bytes, first is the report descriptor, telling host that what's the meaning of every bytes in event, keyboard has a basic example in Device Class Definition for Human Interface Devices which could be used in BIOS, you need to read Appendix B: Boot Interface Descriptors, B.1 Protocol 1 (Keyboard) 和 Appendix E: Example USB Descriptors for HID Class Devices and Appendix E: Example USB Descriptors for HID Class Devices, E.6 Report Descriptor (Keyboard). But those are not enough, mostly two bytes in descriptor made one meaning, the first byte is about type and the second is value, if you want to know how to calculate the first bye, you need to read 8. Report Protocol, and then you'll know why sometimes different numbers have the same meaning because each bits of the byte have different meaning. And their are a lot of Usage tags, they are in another PDF.

I copy https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L28-L144 here and add detailed meaning in comments:

unsigned char kb_report_desc_buffer[]  = {
    // Usage Page (Generic Desktop)
    // I don't know why keyboard should be Generic Desktop.
    0x05, 0x01,
    // Usage (Keyboard)
    // Go to look up in the table.
    0x09, 0x06,

    // Collection (Application)
    // There are collections in descriptor,
    // they represent for data you send to computer.
    // A device should have at least 1 collection.
    // Keyboard's usage page tag is under Application.
    0xA1, 0x01,
    // Report ID (1)
    // You'll find there are no Report ID in the keyboard example,
    // because you can ignore it if you have only 1 collection.
    // But if your keyboard has media keys, they are in different collection.
    // So you need a Report ID and you send it as the first byte of event.
    // You know why we send 9 bytes in event while people say 8 for keyboards?
    0x85, 0x01,

    // Usage Page (Keyboard)
    // Usage Page is different from Usage.
    0x05, 0x07,
    // Usage Minimum (224)
    // You have many Usages in a collection, but you cannot list them all,
    // so you have a range from 224, which is Left Control.
    0x19, 0xE0,
    // Usage Maximum (231)
    // And end to 231, the Right GUI, those are 8 modifier keys.
    // Left Control,Right Control,Left Alt,Right Alt,Left Shift,Right Shift,
    // Left GUI,Right GUI。
    // But when you convert hex to binary, the right is bit 0,
    // so reverse the sequence.
    0x29, 0xE7,
    // Logical Minimum (0)
    // 0 is the minimum of a bit.
    0x15, 0x00,
    // Logical Maximum (1)
    // Press is the maximum 1.
    0x25, 0x01,
    // Report Size (1)
    // One key for one bit.
    0x75, 0x01,
    // Report Count (8)
    // 8 keys.
    0x95, 0x08,
    // Input (Data, Variable, Absolute): Modifier byte
    // Too long to describe what input means, please read the table,
    // should match the above.
    0x81, 0x02,

    // Reserved, just send a byte of 0.
    // Report Size (8)
    0x75, 0x08,
    // Report Count (1)
    0x95, 0x01,
    // Input (Constant): Reserved byte
    0x81, 0x01,

    // LEDs on keyboard, HID keyboard does not handle LED states itself.
    // This is not a real keyboard so just grab it from Internet.
    // 5 LEDs works like modifiers but output.
    // Usage Page (LEDs)
    0x05, 0x08,
    // Usage Minimum (1)
    0x19, 0x01,
    // Usage Maximum (5)
    0x29, 0x05,
    // Report Size (1)
    0x75, 0x01,
    // Report Count (5)
    0x95, 0x05,
    // Output (Data, Variable, Absolute): LED report
    0x91, 0x02,

    // 5 bits of LEDs are not aligned so add 3 bits as padding.
    // Report Size (3)
    0x75, 0x03,
    // Report Count (1)
    0x95, 0x01,
    // Output (Constant): LED report padding
    0x91, 0x01,

    // We have 101 normal keys on keyboards so you don't want to use 101 bits
    // for them (it might be OK but hard to write), so we return an array and
    // a byte is for a keycode.
    // Usage Page (Key Codes)
    0x05, 0x07,
    // Usage Minimum (0)
    // Minimum is 0 for no key pressed.
    0x19, 0x00,
    // Usage Maximum (101)
    // 101 is the max keycode on standard keyboard.
    0x29, HID_KEYBOARD_KEYS - 1,
    // Logical Minimum (0)
    // Bytes are keyboards and the minimum is 0.
    0x15, 0x00,
    // Logical Maximum(101)
    // Maximum is 101.
    0x25, HID_KEYBOARD_KEYS - 1,
    // Report Size (8)
    // Each key takes 1 byte.
    0x75, 0x08,
    // Report Count (6)
    // USB keyboards need to report 6 keys at lease,
    // your keyboard might tell your computer it can report more,
    // but 6 is mostly OK.
    0x95, HID_KEYBOARD_MAX_KEYS,
    // Input (Data, Array): Keys
    // Input is an array here, not a variable, array can only be the last
    // of a collecton.
    0x81, 0x00,

    // End Collection
    // Collection for keyboard is end.
    0xC0,

    // Usage Page (Consumer)
    // FOr media keys.
    0x05, 0x0C,
    // Usage (Consumer Control)
    0x09, 0x01,

    // Collection (Application)
    // A new collection because data format changes.
    0xA1, 0x01,
    // Report ID (2)
    // A new Report ID, so event has two bytes, first Report ID then keycode.
    0x85, 0x02,

    // Usage Page (Consumer)
    0x05, 0x0C,
    // Like the modifiers, but those usages are not consistent,
    // we have to list them here.
    // Usage (Scan Next Track)
    0x09, 0xB5,
    // Usage (Scan Previous Track)
    0x09, 0xB6,
    // Usage (Stop)
    0x09, 0xB7,
    // Usage (Eject)
    0x09, 0xB8,
    // Usage (Play/Pause)
    0x09, 0xCD,
    // Usage (Mute)
    0x09, 0xE2,
    // Usage (Volume Increment)
    0x09, 0xE9,
    // Usage (Volume Decrement)
    0x09, 0xEA,
    // Like modifiers.
    // Logical Minimum (0)
    0x15, 0x00,
    // Logical Maximum (1)
    0x25, 0x01,
    // Report Size (1)
    0x75, 0x01,
    // Report Count (8)
    0x95, 0x08,
    // Input (Data, Array)
    0x81, 0x02,

    // End Collection
    0xC0
};

So our keyboard sends this to host. The only thing you need to remember is send Report ID as the first byte of event. Then how to convert SDL event into HID event? Firstly I read @amosbird's code and he thinks HID event is like SDL event——one field for modifiers and another field for press/release and one field for which key, an event represents for one single key——but it's not, HID keyboards won't tell you which key is pressed or release, it's based on sequence, for example I press C first, it sends C 00 00 00 00 00, then I press B, sends C B 00 00 00 00, release C, sends B 00 00 00 00 00, the host is responsible for comparing the events and getting info like "B pressed" or "C released". So we need to convert it back to "what are pressed keys", it's simple, just use an array for state of current keys, update it when a SDL event is fired (not all key events are the 101 keys), then iterate the array to generate a event contains the indices of true, the sequence of keys does not matter in HID and SDL's scancode is the same as HID values. For modifiers are easier, every SDL events contains all modifiers just like HID events, they are just different in bits, so we do a convert (https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L195-L223). But don't ignore events just because it's not inside the 101 keys! User may just press a modifer, and we only skip updating keys state here (https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L225-L269>).

You may ask that what will happen if I press 7 keys at the same time? HID requires to report a phantom state, which fills 0x01 as all 6 keys (https://github.com/AlynxZhou/scrcpy/blob/dev/app/src/hid_keyboard.c#L250-L259).

We almost done it, and if you have a DE, media keys should be caught by it before SDL, so I never send it in code. Scrcpy uses some combination keys for it. And some cleaning, for example @amosbird forget to unregister HID device before program exitting, and you cannot use soft keybaords until you disconnect the USB cable.

At last @rom1v says scrcpy uses another thread for input, so I make a thread for AOA, too. Just grab the code from controller.

You may ask about HID mouse, I've tried it, not good, AOA can register different HID devices, just pass different ID as argument, I have those code. But the biggest problem is that HID mouse only report the delta of X and Y, if I move the mouse outside the window from left edge, MotionEvent stops, the HID cursor will stop on the edge, and then I move my mouse inside the window from right edge, the HID cursor then loses sync with my mouse. The typicall scrcpy injection events work better so I give up.

libusb has an old bug on Windows so this cannot work on windows, I have no idea and because at first I just want to use QQ on Linux, I think it's OK, so does @rom1v. My code is correct and once libusb fixed it, my code should not be updated I think.

However you need USB and this won't work with ADB over WiFi, I typically charge my phone while using computer so it's not a problem for me.

PR is https://github.com/Genymobile/scrcpy/pull/2632/files, only 1 thousand lines? I spend a lot of time on it.

by Alynx Zhou (alynx.zhou@gmail.com) at September 19, 2021 04:01 AM

September 16, 2021

ヨイツの賢狼ホロ

如何比较安全的配置 Web 服务器 - TLS 和 HTTPS

~~嗯?这家伙鸽了多久了?~~

又是一次知道的已经做好了,不知道的对着他耳朵唠叨也不会去做的系列节目。(嗯?) 如果不想听咱啰嗦的话, 直接去看 Mozilla Infosec 的相同主题的内容就好啦。

HTTPS 和 TLS 到底是什么关系?

HTTPS 其实就是包在 SSL/TLS 里的 HTTP 啦,考虑到 SSL 3.0 早在 2015 年就废弃了,现在基本上就只提 TLS 了。(虽然 TLS 1.0 和 1.1 也在 2020 年被废弃了就是。)

因为 HTTPS 能够加密所交换的数据、在传输的过程中保证数据的完整性,以及证明用户是在与目标网站进行通信。所以很多组织都在呼吁让整个互联网的流量都是加密的。 以及各大浏览器也在逐渐的将只支持 HTTP 的网站列入“不安全”的范围。(虽然也有人对这么做有没有作用存疑。)

取得一份证书

关于 TLS 的实现原理不是咱的重点(其实是咱也不是很懂,汝也许可以看看 IETF 的 RFC, 如果真的有必要的话。 汝现在需要知道的最重要的一点就是,要使用 HTTPS 的话,网站管理员要先从大家都信任的某些地方(就是 CA/数字证书认证机构 啦)取得一份带有私钥的证书才能继续接下来的步骤。

有不止一家企业和组织营运的 CA 负责签发证书,例如历史悠久的 Entrust 和 DigiCert ,和数个公司或组织为了普及 HTTPS 创办的 Let's Encrypt 等等。

如果汝还没有预算购买证书的话,可以先从 Let's Encrypt 开始

不过 Let's Encrypt 的证书有效期只有三个月,所以如果汝对自己的服务没有完整的命令行访问权限(例如比较常见的虚拟共享主机)的话,可能会稍微麻烦一些。

至于怎么申请证书嘛, Let's Encrypt 推荐使用它们制作的 certbot 帮助程序。 可以在这里查阅文档, ~~咱就先跳过了(笑)~~

如果希望只允许特定的 CA 给汝的网站签发证书的话,可以通过 DNS 证书颁发机构授 权记录(简称 CAA)来实现,CA 会提供相应的指南告诉汝如何添加和修改这种记录。

规划网站的兼容目标

越新的标准越能提升安全性,越旧的浏览器就越有可能不支持,大概就是这个样子。所以根据自己网站的受众决定向后兼容的程度就很重要。Mozilla 的标准有三个等级:

  • 现代等级:只支持 TLS 1.3,适用于不需要向后兼容的新网站。
  • 中等等级:支持 TLS 1.2 和 1.3,适用于大多数网站和估计近五年间的设备。
  • 向后兼容等级:只有汝需要考虑支持像是 Windows XP 或者 Java 6 这种相当老的系统时才应该使用这个等级。

它们分别兼容的最旧版本的浏览器和操作系统在下面:

等级 兼容的最旧版本
现代 Firefox 63, Android 10.0, Chrome 70, Edge 75, Java 11, OpenSSL 1.1.1, Opera 57, and Safari 12.1
中级 Firefox 27, Android 4.4.2, Chrome 31, Edge, IE 11 on Windows 7, Java 8u31, OpenSSL 1.0.1, Opera 20, Safari 9
向后兼容(旧的) Firefox 1, Android 2.3, Chrome 1, Edge 12, IE8 on Windows XP, Java 6, OpenSSL 0.9.8, Opera 5, and Safari 1

Mozilla 也提供一个线上配置生成器,可以根据不同的等级和服务器软件生成合适的参考配置文件。 根据汝的需要稍加修改(例如证书的位置)应该就可以使用了。

正确的重定向 HTTP 链接

现在汝的网站有 HTTPS 版本了,很好。但只有用户主动的输入带有 https:// 的地址的时候才能访问 HTTPS 版本, 于是汝还需要让用户通过 HTTP 访问的时候能自动重定向到 HTTPS 版本。

对于目前比较常见的 Apache 和 Nginx 来说,都挺简单的。

# Nginx 示例配置。
server {
  listen 80;

  return 301 https://$host$request_uri;
}
# Apache 的示例配置,记得把 foo.tld 换成汝自己的域名。
<VirtualHost *:80>
  ServerName foo.tld
  Redirect permanent / https://foo.tld/
</VirtualHost>

HTTP 严格传输安全 (HSTS)

汝设置好了正确的重定向,这本来没有问题,直到攻击者劫持了访问汝的网站第一步的 HTTP 响应…… 一场典型的 SSL 剥离攻击开始了。

所以 IETF 为了补救这个缺陷提出了 HTTP 严格传输安全标准,汝也许经常能看到它的缩写 HSTS 。

要实现 HTTP 严格传输安全,核心就是一个 HTTP 首部,例如下面的这个:

Strict-Transport-Security: max-age=31536000; includeSubDomains

如果 https://example.com 的响应中有这段的话:

  • 在接下来的 31536000 秒(即一年)中,浏览器向 example.com 发送 HTTP 请求时,必须采用 HTTPS 来发起连接。 比如,用户点击超链接或在地址栏输入 http://example.com/ ,浏览器应当自动将 http 转写成 https, 然后直接向 https://example.com/ 发送请求。
  • 在接下来的一年中,如果 example.com 服务器发送的TLS证书无效,用户不能忽略浏览器警告继续访问网站。
  • 这也适用于 example.com 下面的子域名,例如 www.example.com 。

以及这段只有在 HTTPS 响应中才会生效,那汝应该也会想那么第一次访问网站时怎么办。 各大浏览器的做法是维护一个预先加载列表,在列表上的网站总是会通过 HTTPS 访问。 汝可以参考这个网站来了解将自己的网站加入预先加载列表的要求。

OCSP 装订

汝也许听说过一个叫 OCSP(在线证书状态协议)的东西。浏览器每次连接支持 OCSP 的网站时,都会向 CA 发送请求查询这个证书的状态。 但这就带来了两个问题:

  • OCSP 是明文发送的,汝和 CA 之间的任何人都能知道汝在访问哪个网站,虽然不能精确到 URL,但进行阻断的话已经足够了。
  • 如果 CA 提供 OCSP 查询的服务器偷偷的留下了访问的记录……

而 OCSP 装订则改为了服务器向 CA 获取 OCSP 响应并缓存一段时间,用户访问时只要验证从服务器发送来的 OCSP 响应的有效性即可。

# Nginx 示例配置。
server {
    ... 
    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # 如果汝的 CA 没有提供 OCSP 使用的证书(例如 Let's Encrypt 就没有),
    # 那么不需要设置 ssl_trusted_certificate 属性。
    ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;

    # 设置成汝服务器使用的 DNS 服务器。
    resolver 127.0.0.1;
}
# Apache 的示例配置。
# OCSP 装订缓存的位置,
SSLStaplingCache shmcb:/tmp/stapling_cache(128000)
<VirtualHost *:443>
...
# 如果汝的 CA 没有提供 OCSP 使用的证书(例如 Let's Encrypt 就没有),
# 那么不需要设置 SSLCertificateChainFile 属性。
SSLCertificateChainFile /path/to/DigiCertCA.crt

SSLUseStapling on
</VirtualHost>

Referrer 策略

Referrer 经常用来表示用户是从哪里来的,也有人担心它会泄露一些隐私信息。 Referrer-Policy 首部用来监管哪些访问来源信息——会在 Referer 中发送——应该被包含在生成的请求当中。

汝可以根据汝的网站和用户选择适合的模式。

# no-referrer-when-downgrade 是现代浏览器的默认设置,
# 在同等安全级别的情况下,引用页面的地址会被发送(HTTPS->HTTPS),但是在降级的情况下不会被发送 (HTTPS->HTTP)。
Referrer-Policy: no-referrer-when-downgrade
# 只有原地址相同时才发送 Referrer。
Referrer-Policy: same-origin
# 对于同源的请求,会发送完整的URL作为引用地址;
# 在同等安全级别的情况下,发送文件的源作为引用地址(HTTPS->HTTPS);在降级的情况下不发送此首部 (HTTPS->HTTP)。
Referrer-Policy: strict-origin-when-cross-origin
# 对不支持 strict-origin-when-cross-origin 的浏览器不发送首部。
Referrer-Policy: no-referrer, strict-origin-when-cross-origin
# 或者直接什么时候都不发送首部。
Referrer-Policy: no-referrer

X-Content-Type-Options 、 X-Frame-Options 和 X-XSS-Protection

X-Content-Type-Options 用来被服务器用来提示客户端一定要遵循在 Content-Type 首部中对 MIME 类型 的设定, 而不能对其进行修改。这就禁用了客户端的 MIME 类型嗅探行为。

# 防止浏览器不正确的识别文件类型,例如把非脚本文件识别成脚本。
X-Content-Type-Options: nosniff

The X-Frame-Options HTTP 响应头是用来给浏览器指示一个页面可否在框架中展现的标记。 (其实还有 <frame>, <embed><object> 的,但这些好像都被弃用了来着。)

站点可以通过确保网站没有被嵌入到别人的站点里面,从而避免被用来进行汝点击的不是实际的地方的那种 clickjacking 攻击。 除了 HTTP 首部以外,内容安全策略(CSP)里的 frame-ancestors 属性也能控制这种行为,但是那个好复杂啊……

# 阻止网页嵌入在框架中。
# 第一行是通过 CSP 设置,第二行是传统的 HTTP 首部形式,它们的效果是相同的。
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
# 只允许相同域名的网页嵌入在框架中。
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN
# 只允许特定来源的网页嵌入在框架中。
# 
Content-Security-Policy: frame-ancestors https://framer.mozilla.org
X-Frame-Options: DENY

X-XSS-Protection 响应头是 Internet Explorer,Chrome 和 Safari 的一个特性, 当检测到跨站脚本攻击时,浏览器将停止加载页面。

若网站设置了良好的 Content-Security-Policy 来禁用内联 JavaScript ('unsafe-inline'), 现代浏览器不太需要这些保护, 但其仍然可以为尚不支持 CSP 的旧版浏览器的用户提供保护。

# 启用 X-XSS-Protection如果 mode  block 的话
# 浏览器将在检测到可能的跨站脚本攻击时停止加载而不是清空页面
X-XSS-Protection: 1; mode=block

至于那个最复杂的 CSP 嘛…… ~~下次再说了……~~

更多资源

除了上面提到的 Mozilla 的文档和配置生成器以外:

  • Qualys SSLlab 可以帮汝测试汝的 HTTPS 配置, 也提供了一些可以参考的文档。
  • BadSSL可以测试汝正在使用的浏览器遇到错误的 TLS 设置时的反应。

by Horo at September 16, 2021 04:00 PM

September 08, 2021

Alynx Zhou

Fontconfig 和 Noto Color Emoji 和抗锯齿

我一直用的是以前积攒的一份 fontconfig 配置,主要功能就是设置对于无衬线字体优先用 Roboto 显示英文字体,然后回退到 Noto Sans CJK SC 显示中文字体,因为 Roboto 比 Noto 的英文字好看,以及对等宽字体优先用 Monaco。虽然大部分都是网上抄来的,我自己并不太懂,但是这个配置一直工作的还可以。直到我开启了 RIME 内置的 emoji 输入法,发现 emoji 显示成了空白。

一开始没觉得是什么很难解决的问题,在字体列表末尾加上 Noto Color Emoji 不就行了?不过事情要是真的这么简单,也就没必要写个博客记下来了。忘记当时怎么查的了,总之是搜到 一个 firefox 的 bug,提 bug 的人表示自己一直是开着 hintfull 和 antialias 的,关掉这个 Noto Color Emoji 才能显示。于是我看了一眼我的配置,也有这么一段:

<!-- 针对所有字体的默认设置,力求显示效果最好。 -->
<match target="font">
  <!-- 禁用内嵌点阵。 -->
  <edit name="embeddedbitmap" mode="assign">
    <bool>false</bool>
  </edit>
  <!-- 禁用合成粗体。 -->
  <edit name="embolden" mode="assign">
    <bool>false</bool>
  </edit>
  <!-- 设置显示器为 RGB 排列。 -->
  <edit name="rgba" mode="assign">
    <const>rgb</const>
  </edit>
  <!-- 开启轻度微调。 -->
  <edit name="hinting" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="autohint" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="hintstyle" mode="assign">
    <const>hintslight</const>
  </edit>
  <!-- 开启抗锯齿。 -->
  <edit name="antialias" mode="assign">
    <bool>true</bool>
  </edit>
</match>

那我觉得针对 Noto Color Emoji 关掉这几个选项就行了,正好这个 bug 下面就有人提供了配置:

<!-- Noto Color Emoji 不支持开启抗锯齿和微调,所以在全局开启之后还得给它关掉。 -->
<match target="font">
  <test name="family" qual="any">
    <string>Noto Color Emoji</string>
  </test>
  <edit name="scalable" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="embeddedbitmap" mode="assign">
    <bool>true</bool>
  </edit>
  <edit name="hinting" mode="assign">
    <bool>false</bool>
  </edit>
  <edit name="antialias" mode="assign">
    <bool>false</bool>
  </edit>
</match>

重新登录,输入法里面的 emoji 都显示出来了,我以为事情到这里就结束了,没想到噩梦才刚刚开始……

某一天用 firefox 打开 twitter,发现里面字体都是毛毛糙糙的,我觉得很离奇,明明我只是对 Noto Color Emoji 关了抗锯齿,为什么 twitter 的字体也受到影响了?而且其他页面的字体都是正常的,后来我又打开过一个微软的文档页面,也出现了类似的毛病。我研究了一下发现这两个页面都用了自定义的 webfont。可是 webfont 和 Noto Color Emoji 有什么关系?人类摸不着头发。

我查了很多博客文章,不过它们都关注在正常的字体顺序上,还没有人研究 webfont 有什么问题。于是我只能自己研究,不过我不能保证我对 fontconfig 的理解完全正确,这东西看起来非常复杂,不过我还是大致搞清楚了一些事情。引用我看到的 一篇英文博客 的标题:

当我凝视着 fontconfig 时,fontconfig 也在凝视着我。

首先我找到了这个叫 fontconfig 几个常见的坑 的文章,里面信息量挺大的,不过说实话没有解决掉我的问题——我明明只对匹配到 Noto Color Emoji 时候关掉了抗锯齿,为什么 webfont 也受到影响了呢?这篇文章主要介绍怎么调整字体列表顺序,不过说实话我没有遇到那么多障碍,简单的写法就工作了。同一个作者还写了 Color Emoji in openSUSE 的文章,不过总感觉是交待到一半,翻了文章列表没有后续了。而且这篇文章关心的也是字体 fallback 时候某些字符选到错误的字体的问题,和我这个不太相关。

然后还有 Linux fontconfig 的字体匹配 这篇文章,它介绍了浏览器是怎么和 fontconfig 合作匹配字体的,但是对于 webfont 也是没有提到。包括它的后续文章 用 fontconfig 治理 Linux 中的字体 也没提到和 Noto Color Emoji 相关的调整(可能和它用的 Twemoji 有关系)。

然后我尝试按照文章里的办法对 firefox 进行一个 fontconfig 的调试,FC_DEBUG=4 firefox --new-instance --private-window https://twitter.com/ 直接吐了 12 万行输出,我实在是不知道从哪里看了。翻了一下午胡乱修改了好几次配置也没找出来哪里有问题。就在我眼花的时候偶然看到一条有用的,fontconfig 吐出来一个匹配结果,family 是 Roboto, Noto Sans CJK SC, Noto Color Emoji, sans-serif… 等等一长串,style 是 。这个就很有趣了,因为我看 firefox 里面,twitter 用的 webfont 叫 TwitterChirp,它的 style 也是这么个 (我怀疑是个 bug),那这条可能就是相关的了。然后我发现这个匹配结果下面 antialias 是 false,但理论上来说这条又不是最先匹配到 Noto Color Emoji,怎么会应用 Noto Color Emoji 的设置呢?我猜测是因为默认 test 语句是 qual="any" 含义似乎是只要匹配到的有一个满足就应用,正常匹配一个字体应该是不会返回这么一长串 family 的(我用 fc-match 一般只返回一个符合条件的 family),虽然我不知道为什么 webfont 会返回这么一长串结果,总之我改成 qual="all" 也就是必须所有结果都是 Noto Color Emoji 时候才应用就好了对吧!然后重新登录,emoji 能显示,我以为已经胜利了!结果事情证明是半场开香槟……

当我以为完全没问题,正打算用 atom 整理一下这份配置的时候,突然发现不对劲了……Atom里面的中文字体失去了抗锯齿……你可以想像我当时心情有多绝望。具体为什么还是同样的我不知道,不过我猜测恰好是和 Linux fontconfig 的字体匹配 里面提到的 chromium 查询字体的奇葩逻辑有关系……可能只是它某次查询刚好漏掉了 Noto Sans CJK SC 匹配到了 Noto Color Emoji 然后就给了个 antialias false,然后再通过什么奇怪的 UI 字体查询找出了中文字体……反正总之我的心好累,我实在不知道该怎么解决了,我只想用个 emoji,我甚至还给 Noto Color Emoji 提了请求支持抗锯齿的 issue……然后我决定要不换成 Twemoji 算了。

Twemoji 倒也不是开箱即用,它不能禁用内嵌点阵字体,不过好歹这个不像抗锯齿那么要命,就算影响了其他字体大概也许能够忽略吧……于是我改成了这个。不过这时候我突然灵光一闪有了新主意。

按照前面那几篇博客和 这一篇手册翻译 的讲解,fontconfig match 的 target 分为三个阶段,第一是 scan,扫描字体文件,构建一个集合。然后是 pattern,按照规则构建一个字体列表。最后是 font,意味着已经挑出了需要的字体列表。所以调整字体回退顺序一般都放在 pattern 阶段。而网上大部分都把对于 freetype2 微调的选项放在了 font 阶段,不过我想放在 scan 阶段是不是也可以?这样扫描到 Noto Color Emoji 的时候就对它设置选项,也就不存在 family 列表可能有很多项的问题了。测试了一下发现基本一切正常,于是就把所有 freetype2 微调选项移到了 scan 阶段。(我不是 fontconfig 的专家,这一段要是有错误还希望指正。)

下面是我现在的配置,首先我配置文件放的位置是 /etc/fonts/local.conf,因为 /etc/fonts/fonts.conf 是发行版默认的设置,用来加载其他配置文件所以不能改,然后我想对所有用户都生效,所以没有放在我的家目录,Arch Linux 包含一个 /etc/fonts/conf.d/51-local.conf 文件,/etc/fonts/fonts.conf 会加载它,然后它再加载 /etc/fonts/local.conf

首先是 XML 开头必须要有的那些东西:

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>

然后接下来就是针对所有字体的 freetype2 微调选项,按照上面介绍的放在 scan 阶段:

  <!-- 针对所有字体的默认设置,力求显示效果最好。 -->
  <match target="scan">
    <!-- 禁用内嵌点阵。 -->
    <edit name="embeddedbitmap" mode="assign">
      <bool>false</bool>
    </edit>
    <!-- 禁用合成粗体。 -->
    <edit name="embolden" mode="assign">
      <bool>false</bool>
    </edit>
    <!-- 设置显示器为 RGB 排列。 -->
    <edit name="rgba" mode="assign">
      <const>rgb</const>
    </edit>
    <!-- 开启轻度微调。 -->
    <edit name="hinting" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="autohint" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="hintstyle" mode="assign">
      <const>hintslight</const>
    </edit>
    <!-- 开启抗锯齿。 -->
    <edit name="antialias" mode="assign">
      <bool>true</bool>
    </edit>
  </match>

内嵌点阵的效果通常没有矢量绘制的效果好,合成粗体也只是个临时方案,微调什么的见仁见智了就。

然后对于 Twemoji 要打开内嵌点阵:

  <!-- 当然另一个简单方案是换成 Twemoji,不过它不能关这个。 -->
  <match target="scan">
    <test name="family" qual="any">
      <string>Twemoji</string>
    </test>
    <edit name="embeddedbitmap" mode="assign">
      <bool>true</bool>
    </edit>
  </match>

对于 Noto Color Emoji 也是打开内嵌点阵关闭微调和抗锯齿:

  <!-- Noto Color Emoji 不支持开启抗锯齿和微调,所以在全局开启之后还得给它关掉。 -->
  <match target="scan">
    <test name="family" qual="any">
      <string>Noto Color Emoji</string>
    </test>
    <edit name="scalable" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="embeddedbitmap" mode="assign">
      <bool>true</bool>
    </edit>
    <edit name="hinting" mode="assign">
      <bool>false</bool>
    </edit>
    <edit name="antialias" mode="assign">
      <bool>false</bool>
    </edit>
  </match>

我个人还有一个需求,Monaco 作为一个等宽字体竟然支持连字,导致某次我看别人的代码时候 fi 连在一起让我以为他没有对齐缩进,结果人家说是我的问题,十分尴尬,于是我在这里关掉了它。不过有些程序比如 firefox 是不支持这个的,解决方法是用 FontForge 编辑字体文件删掉连字相关的数据再导出……

  <!-- 我真不理解一个等宽字体要连字功能干嘛?故意变得不等宽? -->
  <match target="scan">
    <test name="family" qual="any">
      <string>Monaco</string>
    </test>
    <edit name="fontfeatures" mode="append">
      <string>liga off</string>
      <string>dlig off</string>
    </edit>
  </match>

然后就是常见的默认字体代称(sans-serif,serif,monospace,emoji)回退列表了,我是按照 Roboto/Roboto Slab/Monaco -> Noto Sans/Serif (Mono) CJK SC -> Noto Color Emoji 的顺序回退的:

  <!-- 下面这一系列等同于 alias。 -->
  <!-- 默认无衬线字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>sans</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <match target="pattern">
    <test name="family" qual="any">
      <string>sans-serif</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <!-- 默认有衬线字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>serif</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Roboto Slab</string>
      <string>Noto Serif CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <!-- 默认等宽字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>monospace</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Monaco</string>
      <string>Noto Sans Mono CJK SC</string>
      <string>Noto Color Emoji</string>
    </edit>
  </match>

  <!-- 默认 emoji 字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>emoji</string>
    </test>
    <edit name="family" mode="prepend" binding="same">
      <string>Noto Color Emoji</string>
    </edit>
  </match>

这里的写法和 <alias> 是等价的。设置完这个记得把所有软件的自定义字体设置里面无衬线设置为 sans-serif,有衬线设置为 serif,等宽设置为 monospace,这样才会遵守这里的回退顺序。

然后很多网站写的都是 Apple Color Emoji,我们这里自然是没有的,要换掉:

  <!-- 替换 Apple Color Emoji 字体。 -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>Apple Color Emoji</string>
    </test>
    <edit name="family" mode="assign" binding="same">
      <string>Noto Color Emoji</string>
    </edit>
  </match>

然后是个非常弱智的部分,B 站直播首页的 CSS 写死字体回退顺序为 Arial,Microsoft YaHei, "Microsoft Sans Serif",Microsoft SanSerf,微软雅黑,你没看错,这个不合格的前端连最后要加 sans-serif 保证兼容性都不知道,而且还把 Microsoft Sans Serif 拼错了,如果我是他的老板,我真想一拳打在他头上告诉他这个世界不是只有微软字体,然后再把他开除掉。但现在我需要加条规则让他匹配到我想要的字体,虽然这样在一些需要微软雅黑的 office 软件里面可能有问题,不过反正我的工作不需要用垃圾 office 哈哈哈哈哈哈哈哈哈哈:

  <!--
    B 站直播首页的前端认为这个世界上只有微软字体(Arial,Microsoft YaHei,
    "Microsoft Sans Serif",Microsoft SanSerf,微软雅黑),
    所以我不得不开着这几个规则。如果我是他的老板,我就会开除掉这个不合格的前端。
    这里不能换成 sans-serif,因为它们只是插入用的锚点而不是子列表,
    除非我们把这条抬到最前面。
  -->
  <match target="pattern">
    <test name="family" qual="any">
      <string>Microsoft YaHei</string>
    </test>
    <edit name="family" mode="append" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
    </edit>
  </match>

  <match target="pattern">
    <test name="family" qual="any">
      <string>Microsoft Sans Serif</string>
    </test>
    <edit name="family" mode="append" binding="same">
      <string>Roboto</string>
      <string>Noto Sans CJK SC</string>
    </edit>
  </match>

剩下还有一些冗长的用来在不同语言下选择不同的 Noto Sans CJK 变体的设置,因为 Noto Sans CJK 是一个多语种集合字体,然后中日韩对一些汉字有不同的变体,所以需要这一段,不过我就不贴在这里了,完整的文件可以在文章末尾下载。

最后收个尾:

</fontconfig>

完整的文件在这里:local.conf

😼

by Alynx Zhou (alynx.zhou@gmail.com) at September 08, 2021 01:33 AM

August 26, 2021

百合仙子

倾听蓝牙耳机的按键事件

本文来自依云's Blog,转载请注明。

缘起

我的蓝牙耳机有简单的多媒体按键:上一曲、下一曲、播放、暂停。这几个按键在 Android 手机上是开箱即用的,然而在 Arch Linux 上,尤其是我的 Awesome 桌面环境上,并不那么自动。

其实按键事件都能收到的啦。可以收到 XF86AudioPrev, XF86AudioNext, XF86AudioPlay, XF86AudioPause 这么几个按键。给它们绑定热键,去调用 MPRIS 就好了。我使用的是 playerctl 工具。mpv 的 MPRIS 支持用的是 mpv-mpris。火狐自动就支持了,不用做什么。

看起来这样就好了?我也以为如此,直到我离电脑远了一些……

问题

躺在床上玩手机时也可以用电脑听歌啦~你问我为什么不用手机听歌?因为我的电脑没有 NFC 功能,耳机切到手机碰一下就可以了,可是切到电脑上是要打命令抢连接的!所以就不切来切去啦,反正手机上的曲库和电脑上是同步的。

可是!耳机多媒体按键怎么不管用了呢?我瞟了一眼电脑,哦,它怎么屏幕还亮起来了?反复几次之后,我终于搞明白了——锁屏的时候按键事件全被锁屏软件给挡下来啦……

那怎么办呢?

我有看到 acpid 那边也收到了些事件,比如「cd/prev / CDPREV」和「cd/next / CDNEXT」。但是不是很稳定,时有时无,也没看到播放和暂停。再加上从作为系统服务的 acpid 将指令传到用户会话比较麻烦,就放弃了。

后来想到,既然能收到按键事件,那么应当有个输入设备在。xinput 看了一下,果然有个「WH-1000XM2 (AVRCP)」,用 evtest 在 /dev/input/ 也能找到对应的设备文件。那直接读这个设备文件不就好了?

解决

好是好,但是没权限啊。不过像/dev/video*之类的文件就有权限,是 systemd 拿 udev 规则给加上的。我之前也给 i2c 设备加过权限,只是那次是直接 chmod 了,这次想试试更优雅的方案——uaccess tag。

这个 uaccess tag 是 systemd 用来给当前会话的用户设备权限的,切换用户会话的时候权限会自动变化。不过没有文档 QAQ,所以只好自己研究了。最终的结果是这样:

SUBSYSTEM=="input", ATTRS{name}=="WH-1000XM2 (AVRCP)", TAG+="uaccess"

这个规则的序号需要小于70,不然赶不上处理 uaccess 的逻辑。sudo udevadm control --reload-rules 然后再 sudo udevadm trigger,就可以看到对应的 /dev/input/event* 文件上已经有 ACL 给我的用户权限了。不过多了写权限,问题不大。

然后就可以开始愉快地找设备文件、读取事件啦。我用 Rust 写的,日常练习嘛,顺便用用前不久看到的 eyre 和 tracing。有个 input-linux 库,不用自己拿 libc 调用 ioctl、定义 C 结构体了。不过这个库不支持从按键名到按键枚举值的转换,所以我 fork 了一下。蓝牙耳机说来来、说走走,所以 inotify 也是少不了的啦。然后还用 toml 整了个配置文件,好放出来给有需要的人用~

啊对了,程序里一上来就把对应的输入设备用 xinput 给禁用了。这样桌面环境就不会收到事件,不会唤醒屏幕,也不会有重复操作了。(不过它退出的时候并没有把设备重新启用,懒~)

代码

by 依云 at August 26, 2021 08:40 AM

August 12, 2021

百合仙子

使用 bwrap 沙盒

本文来自依云's Blog,转载请注明。

bwrap 是命令的名字。这个项目的名字叫 bubblewrap。它是一个使用 Linux 命名空间的非特权沙盒(有用户命名空间支持的话)。

我之前使用过 Gentoo 的 sandbox 工具。它是 Gentoo 用于打包的工具,使用的是 LD_PRELOAD 机制,所以并不可靠。主要用途也就是避免打包软件的时候不小心污染到用户家目录。

使用 bwrap 的话,限制是强制的,没那么容易绕过(至于像 Go 这种因为不使用 libc 而意外绕过就更难得了)。不过 bwrap 不会在触发限制的时候报错。

bwrap 的原理是,把 / 放到一个 tmpfs 上,然后需要允许访问的目录通过 bind mount 弄进来。所以没弄进来的部分就是不存在,写数据的话就存在内存里,用完就扔掉了。这一点和 systemd 也不一样——systemd 会把不允许的地方挂载一个没权限访问的目录过去。

bwrap 的挂载分为只读和可写挂载。默认是 nodev 的,所以在里边是不能挂载硬盘设备啥的。它也提供最简 /proc 和 /dev,需要手动指定。整个 / 都是通过命令行来一点点填充内容的,所以很容易漏掉部分内容(比如需要联网的时候忘记挂载 resolv.conf 或者 TLS 证书),而不会不小心允许不应当允许访问的地方(当然前提是不偷懒直接把外面的 / 挂载过去啦)。

至于别的命名空间,有 --unshare-all 选项,不用写一堆了。如果需要网络,就加个 --share-net(这个选项文档里没写)。没有别的网络方案,因为没特权,不能对网络接口进行各种操作。--die-with-parent 可以保证不会有残留进程一直跑着。

我目前的打包命令长这样:

alias makepkg='bwrap --unshare-all --share-net --die-with-parent \
  --ro-bind /usr /usr --ro-bind /etc /etc --proc /proc --dev /dev \
  --symlink usr/bin /bin --symlink usr/bin /sbin --symlink usr/lib /lib --symlink usr/lib /lib64 \
  --bind $PWD /build/${PWD:t} --ro-bind /var/lib/pacman /var/lib/pacman --ro-bind ~/.ccache ~/.ccache \
  --bind ~/.cache/ccache ~/.cache/ccache --chdir /build/${PWD:t} /usr/bin/makepkg'

以后应该随着问题的出现还会修改的。

其实我学 bwrap 主要不是自己打包啦(毕竟基本上都交给 lilac 了),而是给 lilac 加固。Arch 的打包脚本是 shell 脚本,所以很多时候不执行脚本就没办法获取一些信息、进行某些操作。唉,这些发行版都喜欢糙快猛的风格,然后在上边打各种补丁。deb 和 rpm 的打包也都是基于 shell 脚本的。而 lilac 经常通过脚本编辑打包脚本,或者从 AUR 取,万一出点事情,把不该删的东西给删掉了,或者把私钥给上传了,就不好了。所以前些天我给 lilac 执行 PKGBUILD 的地方全部加上了 bwrap。期间还发现 makepkg --printsrcinfo 不就是读取 PKGBUILD 然后打印点信息嘛,竟然不断要求读取 install 脚本,还要对打包目录可写……

另一个用法是,跑不那么干净的软件。有些软件不得不用,又害怕它在自己家里拉屎,就可以让它在沙盒里放肆了。比如使用反斜杠作为文件路径分隔符写一堆奇怪文件名的 WPS Office。再比如不确定软件会不会到处拉屎,所以事先确认一下。我以前使用的是基于 systemd-nspawn 和 overlayfs 的方案(改进自基于 aufs 和 lxc 的方案所以名字没改),不过显然 bwrap 更轻量一些。跑 GUI 的话,我用的命令长这样:

bwrap --unshare-all --die-with-parent --ro-bind / / \
  --tmpfs /sys --tmpfs /home --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  --ro-bind ~/.fonts ~/.fonts --ro-bind ~/.config/fontconfig ~/.config/fontconfig \
  --bind ~/.cache/fontconfig ~/.cache/fontconfig --ro-bind ~/.Xauthority ~/.Xauthority \
  --ro-bind /tmp/.X11-unix /tmp/.X11-unix --ro-bind /run/user/$UID/bus /run/user/$UID/bus \
  --chdir ~ /bin/bash

其实还可以用来给别的发行版编译东西,取代我之前使用 systemd-nspawn 的方案。bwrap 在命令行上指定如何挂载,倒是十分方便灵活,很适合这种需要共享工作目录的情况呢。以后有需要的时候我再试试看。(好像一般人都是使用 docker / podman 的,但是我喜欢使用自己建立和维护的 rootfs,便于开发和调试,也更安全。)

和 bwrap 类似的工具还有 SELinux 和 AppArmor。它们是作用于整个系统的,Arch Linux 安装会很麻烦,对于我的需求也过于复杂。Firejail 是面向应用程序的,但是配置起来也挺不容易。bwrap 更偏重于提供底层功能而不是完整的解决方案,具体用法可以让用户自由发挥。

by 依云 at August 12, 2021 12:14 PM

August 09, 2021

frantic1048

PLUM - 请问你点的是兔子吗 BLOOM

Tippy and Chino, Rize, Syaro, Chiya, Cocoa

在试图满足客户爸爸的神奇需求而被关进板凳梆硬的小黑屋搬砖的第一天,总算整理好了这波点兔全家桶照片 (´_>`)

Chiya Rize Syaro Chino Cocoa

集齐五位,召唤巨大兔子 Menma and Chino, Rize, Syaro, Chiya, Cocoa Menma and Chino, Rize, Syaro, Chiya, Cocoa

Cocoa and Syaro Chino and Chiya Rize and Syaro Chino, Rize, Syaro, Chiya, Cocoa

August 09, 2021 05:11 PM

July 25, 2021

frantic1048

PLUM - 香风智乃 夏日祭 Ver.

Chino

已经收到了一个多星期,最近终于找到空闲给这位智乃开箱拍照。虽说小伙伴有安利同系列的心爱一起订,但因为我的爱不够就只订智乃了,结果后面还阴差阳错在不同地方订出了两个智乃,这可真是……

普通路过包装盒。 Chino

一圈过去没有啥很需要后期修复的细节,很科学。发饰的搭配非常棒。 Chino Chino Chino Chino

到处都充满了兔子的装饰,以及虽迟但到的提比。 Chino Chino Chino Chino

Chino Chino Chino Chino

July 25, 2021 05:13 PM

July 07, 2021

Phoenix Nemo

修复 LVM XFS 的 Input/output error

某服务挂了。

设备被强制重启之后发现 LVM 满了,但是文件无法访问,所有文件操作显示 Input/output error

查看 dmesg 发现大量文件系统错误,应该是磁盘写满后仍有进程不断读写的过程中被强制断电的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[ 1714.217864] XFS (dm-0): page discard on page 00000000161e11d5, inode 0xd861b703d, offset 937984.
[ 1714.219674] XFS (dm-0): page discard on page 000000001d433e5e, inode 0xd861b703d, offset 942080.
[ 1714.221132] XFS (dm-0): page discard on page 00000000820efe8d, inode 0xd861b703d, offset 946176.
[ 1714.222431] XFS (dm-0): page discard on page 00000000518c8216, inode 0xd861b703d, offset 950272.
[ 1714.223744] XFS (dm-0): page discard on page 00000000753db760, inode 0xd861b703d, offset 954368.
[ 1714.225041] XFS (dm-0): page discard on page 00000000da40787d, inode 0xd861b703d, offset 958464.
[ 1714.226341] XFS (dm-0): page discard on page 00000000ba8adb4b, inode 0xd861b703d, offset 962560.
[ 1714.227629] XFS (dm-0): page discard on page 00000000784c4724, inode 0xd861b703d, offset 966656.
[ 1714.228923] XFS (dm-0): page discard on page 0000000063b2c764, inode 0xd861b703d, offset 970752.
[ 1714.228990] XFS (dm-0): page discard on page 0000000046a36fd8, inode 0xd861b703d, offset 974848.
[ 1714.337426] dm-0: writeback error on inode 58084519997, offset 905216, sector 34365282240
[ 1716.586318] dm-0: writeback error on inode 58084519997, offset 905216, sector 34365309816
[ 1728.444718] xfs_discard_page: 9674 callbacks suppressed

...

[ 1763.990454] XFS (dm-0): xfs_do_force_shutdown(0x8) called from line 955 of file fs/xfs/xfs_trans.c. Return address = 00000000ea9478e4
[ 1763.990459] XFS (dm-0): Corruption of in-memory data detected. Shutting down filesystem
[ 1763.992696] XFS (dm-0): Please unmount the filesystem and rectify the problem(s)

日志写的很清楚了,那就来卸载修复吧

1
2
~> umount /data
~> xfs_repair /dev/mapper/data

显示

1
2
3
4
5
6
7
8
9
10
Phase 1 - find and verify superblock...
- reporting progress in intervals of 15 minutes
Phase 2 - using internal log
- zero log...
ERROR: The filesystem has valuable metadata changes in a log which needs to
be replayed. Mount the filesystem to replay the log, and unmount it before
re-running xfs_repair. If you are unable to mount the filesystem, then use
the -L option to destroy the log and attempt a repair.
Note that destroying the log may cause corruption -- please attempt a mount
of the filesystem before doing this.

咦,为什么还要我 remount。

1
2
3
~> mount -a
~> umount /data
~> xfs_repair /dev/mapper/data

然后就是漫长的等待…(因为是 HDD)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Phase 1 - find and verify superblock...
- reporting progress in intervals of 15 minutes
Phase 2 - using internal log
- zero log...
- 19:33:58: zeroing log - 521728 of 521728 blocks done
- scan filesystem freespace and inode maps...
- 19:34:11: scanning filesystem freespace - 33 of 33 allocation groups done
- found root inode chunk
Phase 3 - for each AG...
- scan and clear agi unlinked lists...
- 19:34:11: scanning agi unlinked lists - 33 of 33 allocation groups done
- process known inodes and perform inode discovery...
- agno = 0
- agno = 15
- agno = 30
...
- agno = 27
- agno = 28
- agno = 29
- 19:43:14: process known inodes and inode discovery - 16555072 of 16555072 inodes done
- process newly discovered inodes...
- 19:43:14: process newly discovered inodes - 33 of 33 allocation groups done
Phase 4 - check for duplicate blocks...
- setting up duplicate extent list...
- 19:43:15: setting up duplicate extent list - 33 of 33 allocation groups done
- check for inodes claiming duplicate blocks...
- agno = 7
- agno = 3
- agno = 8
...
- agno = 30
- agno = 31
- agno = 32
- 19:43:24: check for inodes claiming duplicate blocks - 16555072 of 16555072 inodes done
Phase 5 - rebuild AG headers and trees...
- 19:43:27: rebuild AG headers and trees - 33 of 33 allocation groups done
- reset superblock...
Phase 6 - check inode connectivity...
- resetting contents of realtime bitmap and summary inodes
- traversing filesystem ...
- 19:48:58: rebuild AG headers and trees - 33 of 33 allocation groups done
- traversal finished ...
- moving disconnected inodes to lost+found ...
Phase 7 - verify and correct link counts...
- 19:50:02: verify and correct link counts - 33 of 33 allocation groups done
done

完事后重启,就可以重新访问 LVM 里的文件啦。

后记:

这次所幸根分区是单独的盘,如果根分区和 LVM 在同一块物理盘上的话,需要重启系统进入救援模式,手动激活 LVM 再执行修复。

以及 XFS 还是靠谱呀(看向某每天摸鱼看番剧透的 btrfs 开发者

某服务挂了。

设备被强制重启之后发现 LVM 满了,但是文件无法访问,所有文件操作显示 Input/output error

July 07, 2021 03:05 PM