Arch Linux 星球

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.

Alynx Zhou

A Coder & Dreamer

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

September 17, 2021

Alynx Zhou

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”是完全不一样的态度。

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

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at September 17, 2021 10:51 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

😼

Alynx Zhou

A Coder & Dreamer

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 19, 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

整体使用流程

比如说,要编写这么一小节动词大词的乐谱,有图片,有声音(后文音频下方的链接皆为对应的 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"

July 19, 2021 12:00 AM

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

June 14, 2021

中文社区新闻

替换旧密码散列

libxcrypt 4.4.21 版本开始,新的密码不再接受由弱密码散列算法(比如 MD5 和 SHA1)产生的散列。还在用弱算法保存密码的用户会在下一次登入时收到提示更新他们的密码。
如果登入失败(比如通过显示管理器Display Manager时)请尝试切换到虚拟终端(Ctrl-Alt-F2)然后从那儿登入一次。

by farseerfc at June 14, 2021 01:19 AM

June 08, 2021

frantic1048

FREEing B-style - 本间芽衣子 兔耳 Ver.

Menma usamimi

苦苦等待一年终于收到了这款面码,当初完全没发现到这个手办的,要不是某天晚上一不小心点到了GSC 官网看到了照片导致瞬间心动……结果来说还是很值得的,甚至感觉比当初看到的照片还要更棒一些。只是这个面码是真的,收到快递箱子的时候惊呆了,整个高度比之前最大的大秋千智乃的秋千上的鸟窝还要高一个头。

Menma usamimi

兔耳的发卡是组装的,左右都是防呆的插口,包装的保护也比较完善,很科学。

Menma usamimi Menma usamimi

太可爱了,没时间打字了,全自动快门浪费机启动!(点击图片可查看大图)

Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi

Menma usamimi Menma usamimi Menma usamimi

Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi

Menma usamimi Menma usamimi

尝试了一下新的光照角度,看起来对比好很多。

Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi Menma usamimi

June 08, 2021 12:00 AM

June 04, 2021

frantic1048

EXQ Figure - 忍野忍

Shinobu

今天刚到的小忍(中忍?),和之前的那位不带刀的小忍相比,画风从可爱转向了美型。虽说带着长长的妖刀,但是不会感到凶。

另外最近感觉手办有点记录不过来了,终于注册了很久以前被 Mike Lei 安利的有很多好看的亚丝娜的 MyFigureCollection:https://myfigurecollection.net/profile/frantic1048 ,看起来是个很科学的平台。

Shinobu Shinobu Shinobu

面具和绳子看起来以为是活动的,尝试取下之后才发现是完全固定的,并且绳子也是硬的。头发和躯干的接缝略有点明显,但是只要不拍到就好啦。腿部反光太强了,尝试了拉远灯的距离、调了很久的光的角度、加上轻微的修图依然能感受到……刀的上色很科学,很有金属感。

Shinobu Shinobu Shinobu

Shinobu Shinobu

June 04, 2021 12:00 AM

May 25, 2021

中文社区新闻

大部分官方 IRC 频道移至 libera.chat

你们中一些人可能已经听说了过去几日 freenode.net 的归属权纷争。Arch Linux 和很多别的项目在过去的几十年间一直使用这个 IRC 网络作为讨论和支持的平台。纷争导致很多前 freenode 成员逃离这个网络,成立了新的网络: libera.chat

今日起,Arch Linux 将携其姊妹项目 Arch Linux ARMArch Linux 32 一起将官方 IRC 频道从 freenode.net 迁移至 libera.chat 。请给予我们一些时间以待迁移工作尘埃落定。

我们感谢 freenode 社区多年以来的服务和合作。

by farseerfc at May 25, 2021 02:32 AM

April 25, 2021

Alynx Zhou

新项目和新相机和新住处

本来之前是想等新开的项目搞差不多了就来更新博客,结果没想到越写 TODO 越多一直搞到这个月才搞定,所以到现在才更新。

新项目

从家里回来北京之后第一件事情就是 SUSE 的 HackWeek,我之前大致想好了要做什么,我用过 screenkey 这个项目,但它使用了 X11 的 API,所以不支持 Wayland,我简单调查了一下,发现其实是可以绕过显示部分直接读取输入设备的事件的,于是就打算造一个替代品。

读取键盘事件的部分其实很容易,简单试了一下就完成了。但是反而是显示部分比较难搞。一开始我打算用刚发布稳定版的 GTK4,结果发现 GTK4 在 NVIDIA 驱动的 Wayland 会话下面反而是有问题的,只能回退到没问题的 GTK3。我原本以为一周的时间做出一个能用的程序还是挺充足的,但是后来发现中间有各种各样的问题和奇怪的 work around。比如涉及到 GObject 对象在什么时候释放,有些文档说的也不是很清楚。以及因为要用单独的子进程执行需要 root 权限的后端读取输入事件和用单独的子线程查询后端输出带来的进程/线程间通信问题。篇幅有限,打算后续再开一篇博客来介绍这里面的经验。在一周的时间里勉强做出了能用的 demo 参加了 HackWeek 的成果展示环节(给有始有终的参与者的小礼物大概还放在我公司的桌子上,我都不知道是什么,因为一直没去取),然后用了几周打游戏的时间整理代码里面的问题(主要是各种资源释放),以及做一下翻译工作和打包工作,终于在上周达到基本稳定了。如果有兴趣,可以访问 https://showmethekey.alynx.one/ 或者观看 我发在 B 站上的介绍视频

新相机

之前一直想提升视频的画质,但是还有一个想法是想玩玩摄影,因为平时周末经常是起得很晚然后在家里玩游戏,感觉也挺无聊的,所以打算买个相机,这样周末就有理由出去走走了。拍出来什么东西无所谓,主要是找点乐趣。然后朋友建议我买黑卡,虽然这个玩意确实很黑科技,但我并不是太感兴趣。很久以前我家里有一个数码相机,不是卡片机,是一个能变焦变很长的型号,但是后来智能手机出现之后就很快的不用它了,因为没有手机轻便的同时也没有足够的专业程度。所以我对相机的看法就是要么就搞高级一点的,肯定不会被手机取代,要么就用手机算了,而且说实话最高端的黑卡价格也都达到微单的价格区间了。考虑到要拿来录视频,很多录视频的 UP 主选择的都是微单而不是单反,再查找了一下发现很多人都用的索尼 A73,那选一个大家都在用的总是错不了的吧,于是初步选定了目标。然后又开始考虑是 A73 还是 A7C,虽然很多人对于 A7C 只是缩小体积然后价格却比 A73 高感到不满,但我阅读了一个对比表格之后,觉得还是 A7C 更适合我这种有录视频需求的人,于是就选择了 A7C。

我个人是对 A7C 相比 A73 砍掉的部分没什么需求的,只看参数很容易会变成那种“这个也想要那个也想要”的情况,虽然作为消费者而言,当然是希望厂家提供越多的功能越好。但是在只有这几个选项的情况下还是得考虑需求做一下取舍。比如去掉 MicroUSB 接口导致只能接一些 USB-C 接口的配件,看了看配件价格,短期内我大概是不会考虑这些配件的……去掉前拨轮和摇杆对我来说也不是不能接受,毕竟触摸对焦也挺好用的。虽然少了很多自定义按键,但是方向键现在都是可以自定义的,其实差别也不大。反而轻巧的体积和翻转屏,以及没有录制时间限制于我来说很重要。有人说没有双卡槽不够专业,万一给别人录东西丢了数据没有冗余,但可以预见的未来我不会靠这玩意获得收入,录我自己的话丢了也不算什么无法挽回的后果(反正自己选的)。

看点作品?虽然我就是随便拍拍,还是有几张觉得好看的。

之前去五棵松玩,想试试夜拍效果,结果套头的焦段显然不够拍月亮,不过回家看看感觉这张图还不错。

某天吃完饭散步,坐在长椅上拍了一张,莫名觉得很好看。

之前经常来我窗口咕咕咕叫吵我清梦的珠颈斑鸠,我管它叫大鸽,经常我拿着相机走到窗口它跑了,这张一开始其实拍到了部分室内,导致窗外的部分有点过曝,不过好在 RAW 可以调整,拉低了一些曝光然后裁掉窗户周围的部分,就变得很生动了。

最近大鸽不怎么来了,是不是因为楼下在锯树,大鸽的家没了?(虽然我对珠颈斑鸠再造一个窝需要花的时间表示怀疑。)

买完了 A7C 莫名开始种草 A7S3,4K 60 帧看起来真诱人啊……1080P 60 帧和 4K 30 帧只能二选一到底是谁想出来的牙膏啊!

说起来最近拍照片之后发现存储空间的需求还是很大啊,虽然我两块 2T 的机械应该还足以满足短期内的需求。最近挖矿导致大容量硬盘也涨价了,估计更新存储设备也不太现实,还是有空精简一下吧。NAS 的问题我也考虑过,但是后来想想还是放弃了把星际蜗牛重新搞起来的想法,因为看了很多视频/数码 UP 工作室的介绍之后意识到 NAS 比起存储的用途,更大的用途还是共享,比如多个剪辑师可以同时访问 NAS 上面的一份素材,不用在每台电脑上都复制一份。但我显然没这个需求也没这个网速,只是存储一些数据的话,还是直接插台式机里面比较方便吧!

顺便我朋友一直怂恿我买一个大光圈定焦镜头,我也确实心动了,比较了一下之后打算先入 FE 55 F1.8 ZA,然后再考虑 FE 24-105 G F4 代替套头,不过因为一些原因还是暂时搁置了,原因的话就是下一部分。

新住处

当初因为时间比较紧所以选的这个住处,有几个不太满意的地方:房间太小了,放下柜子床电脑桌就没什么地方了。为了节约空间我是把桌子沿着床边摆的,我自己坐在桌子和衣柜之间,所以如果我要打开衣柜找东西就要把椅子推到桌子下面。然后在我 B 站录的视频里也能看出来,有朋友不止一次吐槽过我录视频环境过于混乱了。房子太旧了,实在是让人连做饭的想法都没,又破又旧,同时上班的话也比较麻烦,不是说交通不便,而是这个房子在一个院子的深处,出门坐公交或者去超市都要走很长一段距离。再加上我的卧室是直接挨着楼道,隔音又比较差,有时候午睡就会被上下楼的脚步声打扰。以及说实话我想要个稍微便宜点的房子,谁不想呢?

所以今年快到期的时候我就在考虑搬家的问题。我们现在还是在家办公的状态,所以相对来说位置不太重要,虽然还是想尽可能找个上班方便的地方。但是很少有人和我一个方向上班,所以折腾了很久。本来是打算搬到更西南边的,甚至这一个月跑了好几个不同地方看房子,但是都因为各种原因没有选中,比如同学上班太远,周围没什么生活设施(虽然就在路边但是出门走好远好远才有超市到底是怎么搞的啊,为什么北京这边经常小区靠着路的一侧都是围墙,没有什么商业网点之类的,真的很没生活感),离地铁站太远,小区门禁是人脸识别(谁允许你们随便采集人脸信息的,而且约朋友来我家还要登记算几个意思啊)。而且想一起合租的同学以后可能考了教师资格证之后回老家工作,那样到时候就要再找人合租,我实在是不抱信心,所以还是决定再找别人合租。

最后和有猫的孙老师在交大附近找了另一个房子。看了一上午最后直接选中的原因只有一个:实在是太新了,刚装修完,非常干净,而且卫生间和厨房都有采光。让我这个饱受破旧和阴暗厨房之苦的人非常满意。同时面积分配也非常科学,孙老师希望要有阳台的卧室,养猫比较方便,我希望卧室面积别太小,这样我录视频的时候把电脑桌当作背景,可以有一定的景深,让桌子在焦外就不会显得太乱。很多房子都是有阳台的卧室大得不得了而另一个卧室只能放个床,这个房子的次卧则不一样,有充足的地方放桌子。所以我们就敲定了这个,兜兜转转还是没离开学校周边。

当然也没有完美的事情,满足了空间和干净和隔音(卧室并不是直接挨着楼道),价格和位置也就不能太强求了,交通的话看起来还好,离路边不会太远,大学周围也不至于太荒凉吧(街头生活感本来就和整齐不沾边,城市到底是让大人物拿来看的地方还是让小人物生活的地方呢?)。虽然考虑到更高的房租+中介费让我感到心疼肉疼(感觉镜头离自己远去了)……但是安慰自己多花几百块买来了刚装修完崭新的房子其实也很赚了对吧!

所以希望接下来能轻松地搬家,毕竟我东西还是挺多的,有了空间就可以仔细布置一下了,让视频看起来更精致一点。

总之还是要对生活有希望吧。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at April 25, 2021 06:50 AM

Leo Shen

Deploying this site on CloudFlare Pages

Historically I've been using my own server to host this site. This leads to very good versatility, but this also means CloudFlare will have to reach the original server every HTML request, and that significantly increases the first request latency. I tried to mitigate that via enabling cache on everything with CloudFlare, but the CDN still has to check periodically that if the cache has expired, which is not ideal.

April 25, 2021 01:00 AM

April 21, 2021

ヨイツの賢狼ホロ

Google 的 FLoC 是个糟糕透顶的主意

翻译自电子前哨基金会 (Electronic Frontier Foundation) DeepLink BLog 的 Google’s FLoC Is a Terrible Idea : https://www.eff.org/deeplinks/2021/03/googles-floc-terrible-idea , 原作者 Bennett Cyphers

EFF 网站的内容以知识共享 署名 4.0 协议授权,本译文也如此。

2021 年 4 月 9 日追记:我们开设了一个名为 "Am I FLoCed" 的网站,如果汝有在用 Google Chrome 浏览器的话,汝可以来看看汝有没有“幸运的”成为 Google 主导的新的“队列联合学习”(Federated Learning of Cohorts ,或者缩写成 FLoC)这一定向广告实验的小白鼠咯。


第三方 Cookie 正在消亡, Google 正尝试创造它的替代品。

没必要为这玩意表示哀悼。二十年来,第三方 Cookie 成了阴暗、肮脏的广告监控行业的关键部分,价值高达数十亿美元;咱们早该淘汰像跟踪 Cookies 这样的 持久的第三方标识符了。不过呢,广告业最大的“玩家”在基础发生变化的时候也不是毫无作为。

Google 正在用一套新技术引领取代第三方 Cookie 的变革。虽然它的提案中的某些部分表示他们并没有从对监控这一商业模型的持续的反对中吸取教训。 这篇文章关注的是提案之一的 “队列联合学习”,这也许是这其中最具野心,破坏性也最大的一部分。

FLoC 意为把原来那些第三方跟踪器做的那些事情转嫁给汝等的浏览器:在这种情况下,汝最近的浏览活动会被归纳为一个行为标签分享给广告商。 这种方法看起来解决了第三方 Cookies 曾经遇到的那些隐私问题,却又在这一过程中产生了新的问题。它也许还会加剧行为广告带来的许多严重的不和隐私相关发问题,像是歧视和劫掠目标。

作为“隐私倡导者”,Google 说 FLoC (和他们声称的“隐私沙盒”里的一 些其他元素)的表现会比现在任由“数据经纪人”和广告巨头肆无忌惮的跟踪和画像更好。 不过这个整个愿景的基础——我们只能从‘老式的跟踪方法’和‘新式的跟踪方法’之中选择一个——本身就是个伪命题。 这并不是个非此即彼的问题。与其重新发明跟踪轮子,咱们为啥不去想象一个没有目标投放广告(和它带来的无数问题)的更好的世界?

这是一条岔路,后方是第三方 Cookies,这也许是互联网发明以来最大的错误。咱们的前方则是两种选择。

一条是,用户可以决定与他们选择互动的每个网站分享什么信息。当用户下次打开一个标签页时,不必担心他们过去的浏览记录会反被用来操纵他们。

另一条则是,每个用户的行为被一目了然的从一个网站传送到另一个网站。他们最近的历史,被提炼成几个片段,被 "民主化",然后和参与每个网页服务的几十个 无名角色分享。用户在每次互动时,都会以“这是我这周的活动,请借此关照我”这种自招的告白开始。

用户和(真正的)隐私倡导者都要拒绝 FLoC 和其他的试图重新发明基于行为的定向广告的错误尝试。 希望 Google 放弃 FLoC 转而为构建真正的用户友好的互联而努力。

FLoC 是个啥?

2019 年,Google 提出了“隐私沙盒”,一种对网络隐私的未来愿景。 它的中心是一套满足现在第三方 Cookies 提供给广告商的诸多使用场景的协议,其中没有 Cookies。 在成立了一个主要由广告商组成的“ Web Advertising Business Group”(网络广告商务工作组?)以后,Google 又向 W3C 提交了这一系列的提案。其后的数个月,Google 和一众广告商又提交了一打以鸟类的名字命名的“技术标准”: PIGIN (这啥?), TURTLEDOVE (斑鸠?), SPARROW (麻雀?), SWAN (天鹅?), SPURFOWL(白鹭?), PELICAN (鹈鹕?, PARROT (鹦鹉?), 以及更多不胜枚举。 每一个“鸟”提案都在实现所谓的定向广告生态系统中一个目前由 Cookie 实现的功能。

FLoC 设计上用来帮助广告商不用第三方 Cookies 进行行为定位。启用 FLoC 的浏览器会收集用户浏览偏 具有相似偏好的用户(这里的“相似”要怎么定义?)会分组到同一队列中。每个用户的浏览器会和网站和广告商分享一份自己属于哪些群组的队列列表。根据他们的提案,每个队列中至少会有几千个用户(虽然没有保证)。

话太长不想看?那汝大可以这么想:汝的 FLoC ID 就是汝最近活动的简单摘要。

Google 的概念验证文档(Proof of Concept,PoC)用每个用户访问的网站的域名作为分组的依据。然后利用名为 SimHash 的算法来生成群组。 这个算法进行的计算可以在用户的设备上完成,因此不需要中心服务器去收集那些行为数据。但是“还是需要一个中心管理员来执行隐私保证”。为了防止队列为了识别用户规划的太小, Google 提议由一个中心统计分配给每个群组的用户数量。如果有的群组用户太少,可以将其与其他类似的群组合并,直到每个群组有足够的用户。

FLoC 对广告商有用的原因是,用户的队列群组会透露关于他们的行为的必要信息。

虽然大部分的细节在提案中还没有定论。这份提案的草稿表示用户的队列 ID 可以通过 JavaScript 访问,但是并没有清楚的指出会不会对可以访问的对象有所限制,以及队列 ID 有无其它的共享方法。 FLoC 可以基于 URL 或页面的内容(而不是之前提起的域名)来集群,也可以使用基于联合学习的系统(它的名字不就是这么暗示的?)代替 SimHash 来生成群组。 它也没有明确队列群组的数量,Google 在实验中使用了 8 位长的队列标识符(也就是队列有 256 个),实际情况下的数量应该会更大;文档中建议的长度是 16 位, 也就是四个十六进制数。队列越多,它们就越具体。更长的队列 ID 意味着广告商了解了更多用户的兴趣,也能更容易的描绘他们的指纹。

不过期限这件要素 ''是'' 约定好的,FLoC 队列会以每周为一个周期利用上周的浏览数据重新计算。这使得 FLoC (看起来)有些难以用于长期识别,但这也使它们更有力地计量了用户在一段时间内的行为。

新的隐私问题

FLoC 意在将定向广告带入保护隐私的未来。不过它的核心设计涉及到了会和广告伤分享新的信息。于是就不出所料的引入了新的隐私风险。

指纹

首先令人担忧的就是指纹。老生常谈的浏览器指纹就是通过收集用户浏览器中离散的信息为这个浏览器创建一个独特和稳定的标记的过程。 EFF 的 “Cover Your Tracks” 演示了这个过程是如何运作的。 一言以蔽之:汝的浏览器看起来越和其他人的不一样,就更容易制作一个独特的指纹。

Google “保证”说绝大多数的 FLoC 队列都会包含几千名用户,因此一个队列 ID 应该不能把汝从数千个同一队列中的其他用户区分出来。然而,这还是给指纹识别者带来了巨大的领先优势。 要是跟踪者从汝的队列 ID 出发的话,它就只需要从几千个(而不是上百万)浏览器中把汝挑出来了。 用信息论的术语来说的话,FLoC 队列包含几比特的,在 Google 的实验中最大是 8 bit。 鉴于这些信息不太可能与浏览器暴露的其他信息相关联,所有它们说不定更加有力。 跟踪者利用现有的独特指纹跟踪一个 FLoC 用户说不定更容易了呢。

Google 也明白这会是个挑战,不过他们却是承诺会在更长远的“隐私预算”计划中来解决指纹的问题。 解决指纹问题是一个令人钦佩的目标,它的提案提供的捷径看起来也很有希望。 不过 FAQ 中又说这“是个早期阶段的建议,目前还没有浏览器实现”, 于此同时,Google 在这个月开始测试 FLoC。

指纹问题已经众所周知的难以阻止,像 Safari 和 Tor Browser 这样的浏览器为了减少指纹攻击面牺牲了大量功能,这个和追踪者进行的消耗战已经持续了数年之久。 缓解指纹追踪的方法一般都会是去除或者限制不必要的熵来源——然后 FLoC 成为了一种新的熵来源。在解决现有的指纹问题之前,Google 还要再制造一个新的问题吗?

跨越内容的暴露

第二个问题不太容易解释:这技术和已经在识别用户的追踪者分享了新的个人数据。FLoC 对广告商有用的原因是,用户的队列群组会透露关于他们的行为的必要信息。

这个项目的 GitHub 页面是这么描述的:

这个 API 将个人的一般浏览历史(以及一般兴趣)的一些信息“民主化”,让任何选择加入的网站都能获得...... 已经知晓某个人的网站(例如他/她/它通过电子邮件地址在这个网站注册)可以记录和展示他们的队列。这意味着有关个人兴趣的信息最终可能会被公开。

就像上面描述的那样, FLoC 队列自己并不能作为标识符。不过能以其它方式标记用户的网站(例如“通过 Google 登录”)就能把通过 FLoC 了解到的信息和个人资料关联。

两类信息可能会因为这种方式暴露:

  1. 关于浏览历史的具体信息。追踪者可能会通过逆向工程队列分配算法来确定任何属于特定群体的用户可能(或者一定?)会访问特定的网站。
  2. 观察者可能会了解到关于人口统计学或兴趣的一般信息。一般来说某一特定群体的成员很可能是某一特定类型的人。例如,某一特定群组可能会有很多年轻的黑人女性用户,或者中年共和党选民;或者 LGBTQ+ 青年。

这就意味着汝访问的网站不需要提前在整个互联网中调查汝就能在第一次接触中知道汝是个什么样的家伙。除此以外,汝的 FLoC 队列会定期更新,能以其它方式标记汝的网站也能借此跟踪汝浏览行为的变化。 别忘了,一个 FLoC 群组就是对汝最近浏览活动的摘要,仅此而已。

汝应该有权在不同的语境中展示汝身份中不同的部分,例如汝在访问一个医疗信息网站的时候会允许它获得汝的健康状况,但是多半不想让它知道汝的政治倾向。 类似的,汝在逛零售网站的时候也多半不想让它知道汝最近有没有阅读关于治疗抑郁症的资料。 FLoC 就这样腐蚀了汝划分的各种语境,取而代之的是把同样的摘要分享给了和汝互动的每一位。

隐私之外

FLoC 被设计来防止通过跨越环境的标识实现个性化的特征分析这一具体问题。 它们的目标都是避免让追踪者获取可以与特定人联系起来的特定信息。 但就像咱们之前展示的那样,FLoC 也能在大量的环境中帮到追踪者。即使 Google 能够通过迭代设计防范这一风险,定向广告的危害并不限于侵犯隐私。 FLoC 的核心目标就和公民自由的其他部分相悖。

定向的力量也是歧视。从定义来看,有针对性的广告允许广告商达到某些种类的人,而排除掉其它的。定向系统可以像为鞋子做广告一样容易的决定谁能看到招聘信息或贷款提议。

这么多年以来,定向广告利用的最多的地方就是剥削、歧视和伤害。关于工作、住房和信贷的广告可以根据目标人群的种族、宗教、性别、年龄或能力等因素不同进行程度的辨别。 基于信用记录或相关历史性特征的定位让高息贷款广告铺天盖地。 基于人口统计学、地点和政治派别的定向广告帮助了一大批出于各种政治动机传播虚假信息和压制选民的传播者。所有类型的行为标的都在增加“令人信服的诈骗”的风险、

与其重新发明跟踪轮子,咱们为啥不去想象一个没有目标投放广告(和它带来的无数问题)的更好的世界?

Google、Facebook 和其它不少的大广告平台已经在设法控制对它们平台的某些用途。例如 Google 限制广告商收集“敏感兴趣类别有关的信息”。 但是没有什么作用,铁了心要这么做的人总能找到解决整个平台对某些类型的定向某些类型的广告的限制的变通方法。

即使平台有绝对的权力知晓哪些信息能用来针对谁,也往往无法阻止对技术的滥用。 FLoC 用一种无监督学习算法来构建它的集群,所以没人能直接控制哪些人会被划分到一起。 (对于广告商的)理想状况来说,FLoC 会把有共同的行为和兴趣爱好的人划分到一起。但网络行为还会和各种敏感特征有关—— 像是性别、种族、年龄和收入这样的人口统计学数据 和“五大”人格特征,甚至心理健康状况。 很有可能 FLoC 也会把具有相同的此类特征的人划分到一起,FLoC 的分组也可能直接反映到与药物滥用、经济困难或支持创伤幸存者有关的网站的访问状况。

Google 提议称它可以通过监控系统的输出来检查和敏感类别的相关性。如果它发现某个队列与某个受保护群体的关系过于密切,管理服务器可以为算法选择新的参数,并告诉用户的浏览器重新分组。

这个解决方案听起来既奥威尔又西西弗,为了监控 FLoC 的分组和敏感分类有多么相关,Google 要用关于用户种族、性别、宗教、年龄、健康和财务状况的数据进行大量审计。每次发现关联太强的队列的时候,就不得不重新配置整个算法并再次尝试,希望新版本中没有其他 "敏感类别 "受到牵连。这个问题比之前在尝试解决的问题更加困难,而且每次解决的尝试都以失败告终。

如果 FLoC 大规模的应用,利用年龄、性别、收入等因素直接区分用户也许会更难,但还不无可能。 坐拥辅助信息的跟踪者可以通过观察和实验学习 FLoC 分组的“意义”——这队列中包含哪些人。 那些铁了心要这么做的人就饿能继续如此辨别用户。 此外,这种行为对于平台来说,将比现在更难监管。有不良意图的广告商必然会以合理的方式进行否认——毕竟它们只是根据行为(而不是那些“敏感的类别”)在接触人们。 整个系统对于用户和监管者来说也会愈发不透明。

Google,别这么做

我们在 Google 第一次提出这些提案的时候曾经把 FLoC 叫做 “隐私保护技术的反面教材”。 我们希望标准流程能阐明 FLoC 的根本性缺陷,让 Google 能重新考虑推进。 事实上,Github 上的几个 Issue 也提出了我们在这里强调的同样的问题。 (例如这个,这个这个。 不过 Google 还是在基础几乎不变的情况下继续开发这个系统。以“95% 有效的” Cookies 取代手段 向广告商夸口。发布于 3 月 2 日的 Chrome 89已经开始测试这一技术。 有一小批 Chrome 用户(大概有上百万)将会(或已经)参加这一实验。

别搞错了,如果 Google 是在按计划在 Chrome 上实现 FLoC 的话,那应该会给大家“选项”。这个系统可能会让从中受益的广告商选择加入,而让觉得会受到损害的用户选择退出。 然后 Google 会在明知绝大多数用户完全不知道 FLoC 是啥,没啥人会选择关闭的的情况下吹捧说这是这是 "透明度和用户控制 "的一步。 自夸说自己终结了在它半生中帮自己赚到上十亿美元的 邪恶的第三方 Cookie,在互联网上开创了一个“新的私密时代”。

不一定非要这样。隐私沙盒中最重要的部分,譬如放弃第三方标识符和对抗指纹识别,将真正更好地改变网络的现状。Google 本可以把那些旧的监视脚手架拆掉的时候不再换上新的既独特又有害的家伙们的。

我们坚决反对 FLoC。那不是我们想要的,也不是用户应有的世界。 Google 该从第三方追踪的经历中学到正确的教训,为用户(而不是为了广告商)设计浏览器。

by Horo at April 21, 2021 02:34 PM

April 12, 2021

Leo Shen

Redesigning this blog

On September 2019, I wrote the first version of this blog. It has been a pretty successful run, but it's time to improve it. To see some context, view release notes for the previous version. What didn't change Many things stays the same. For example, this version of the site is still written with Hugo. I have the idea to switch to Zola, which is written in Rust and has cleaner template language, but Hugo still has the best support for Org-mode.

April 12, 2021 11:00 AM

Phoenix Nemo

FAT32 与 FAT32 的不同

缘由:游戏用的台式机坏掉了。

去年底配了一台 ITX,GIGABYTE AORUS X570 I PRO WIFI 大概是这个型号的主板,在一次 BIOS 升级之后无法进入 POST,配件 LED 都显示正常,风扇正常。于是按照官方说明,准备重刷 BIOS:

  • 一张闪存盘格式化为 FAT32
  • 固件改名为 GIGABYTE.BIN 放在根目录下
  • 插在 BIOS USB 接口,接通电源但不开机的情况下按 Q Flash Plus 开始刷机

但是按下之后并没有描述的橙色灯亮,只看到闪存盘的存取 LED 快速闪了几下就没了动静。

一度以为文件有问题,或者闪存盘有问题,或者格式化有问题。于是拆了一个新的闪迪的闪存盘,重新下载保存固件,依然没有效果。

对着文档回顾一次操作顺序以免有疏漏,然后发现了蹊跷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
~> sudo fdisk /dev/sdb
Welcome to fdisk (util-linux 2.36.2).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): o
Created a new DOS disklabel with disk identifier 0x5f576f17.

Command (m for help): n
Partition type
p primary (0 primary, 0 extended, 4 free)
e extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1):
First sector (2048-15137279, default 2048):
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-15137279, default 15137279):

Created a new partition 1 of type 'Linux' and of size 7.2 GiB.

Created a new partition 1 of type ‘Linux’ and of size 7.2 GiB.

嗯?

Partition Type 是 Linux。这会是读取失败的原因么?于是列出所有的文件系统

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
Command (m for help): l

00 Empty 24 NEC DOS 81 Minix / old Lin bf Solaris
01 FAT12 27 Hidden NTFS Win 82 Linux swap / So c1 DRDOS/sec (FAT-
02 XENIX root 39 Plan 9 83 Linux c4 DRDOS/sec (FAT-
03 XENIX usr 3c PartitionMagic 84 OS/2 hidden or c6 DRDOS/sec (FAT-
04 FAT16 <32M 40 Venix 80286 85 Linux extended c7 Syrinx
05 Extended 41 PPC PReP Boot 86 NTFS volume set da Non-FS data
06 FAT16 42 SFS 87 NTFS volume set db CP/M / CTOS / .
07 HPFS/NTFS/exFAT 4d QNX4.x 88 Linux plaintext de Dell Utility
08 AIX 4e QNX4.x 2nd part 8e Linux LVM df BootIt
09 AIX bootable 4f QNX4.x 3rd part 93 Amoeba e1 DOS access
0a OS/2 Boot Manag 50 OnTrack DM 94 Amoeba BBT e3 DOS R/O
0b W95 FAT32 51 OnTrack DM6 Aux 9f BSD/OS e4 SpeedStor
0c W95 FAT32 (LBA) 52 CP/M a0 IBM Thinkpad hi ea Linux extended
0e W95 FAT16 (LBA) 53 OnTrack DM6 Aux a5 FreeBSD eb BeOS fs
0f W95 Ext'd (LBA) 54 OnTrackDM6 a6 OpenBSD ee GPT
10 OPUS 55 EZ-Drive a7 NeXTSTEP ef EFI (FAT-12/16/
11 Hidden FAT12 56 Golden Bow a8 Darwin UFS f0 Linux/PA-RISC b
12 Compaq diagnost 5c Priam Edisk a9 NetBSD f1 SpeedStor
14 Hidden FAT16 <3 61 SpeedStor ab Darwin boot f4 SpeedStor
16 Hidden FAT16 63 GNU HURD or Sys af HFS / HFS+ f2 DOS secondary
17 Hidden HPFS/NTF 64 Novell Netware b7 BSDI fs fb VMware VMFS
18 AST SmartSleep 65 Novell Netware b8 BSDI swap fc VMware VMKCORE
1b Hidden W95 FAT3 70 DiskSecure Mult bb Boot Wizard hid fd Linux raid auto
1c Hidden W95 FAT3 75 PC/IX bc Acronis FAT32 L fe LANstep
1e Hidden W95 FAT1 80 Old Minix be Solaris boot ff BBT

Aliases:
linux - 83
swap - 82
extended - 05
uefi - EF
raid - FD
lvm - 8E
linuxex - 85

好家伙,看到一个 0b W95 FAT32。直接把分区类型改为 0b 再重新格式化,放入文件,插回 BIOS USB 接口,按下 Q Flash Plus,瞬间电源启动,橙色 LED 开始闪烁,说明已经开始刷写。

查了 Wikipedia,看到这么一段话:

Lists of assigned partition types to be used in the partition table in the MBR were originally maintained by IBM and Microsoft internally. When the market of PC operating systems and disk tools grew and liberated, other vendors had a need to assign special partition types to their products as well. As Microsoft neither documented all partition types already assigned by them nor wanted to maintain foreign assignments, third parties started to simply assign partition types on their own behalf in a mostly uncoordinated trial-and-error manner. This led to various conflictive double-assignments sometimes causing severe compatibility problems between certain products.([1])[http://msdn.microsoft.com/en-us/windows/hardware/gg463525.aspx]

所以也就有了 Partition Type 这样一个细节导致刷写任务失败,原因是 GIGABYTE 在开发 Q Flash Plus 的时候没有考虑过不同厂家、设备、操作系统所建立的 FAT32 分区类型的不同,文档中也没有提及这个问题。顺带又看到了同样有很多 GIGABYTE 主板的用户在网络上表示他们没有办法通过这种方式刷写 BIOS,也难怪会让人困惑了。

再顺便,使用闪迪出厂自带的 FAT32 文件系统无法正常被 Q Flash Plus 识别,是不是也说明了闪迪的闪存盘在出厂时建立的文件系统类型并不是 0b W95 FAT32

总之:

  1. 我的主板终于修好了。
  2. 开发任何功能时要考虑好对不同硬件、系统、环境的兼容性。
  3. 下次买主板,还是乖乖选败家之眼 ASUS ROG 吧(笑

缘由:游戏用的台式机坏掉了。

April 12, 2021 07:19 AM

April 11, 2021

berberman

Subtle cases in GHCi

Posted on April 11, 2021 by berberman

我写这篇文章的原因是无意中发现 head [] 在关闭 ExtendedDefaultRules 的 GHCi 中可以通过类型检查,让我感到很意外;经过一番摸索之后——

前言

GHCi,即 GHC 的交互式(interactive)环境,可以用来求值 Haskell 表达式、加载编译好的 Haskell 程序、交互式推断表达式的类型等等。想必使用 GHCi 是每一位 Haskell 程序员的必备技能。然而,在 GHCi 中类型检查规则与默认情况下的 GHC 略有不同,这可能在某种程度上会带来一定困惑。在这篇文章中,我们将一起认识带来这个不同的语言扩展,并通过 GHC 以及 GHCi 的源码了解语句在 GHCi 中是怎样被执行、并打印的。

GHCi 中默认启用的语言扩展

一些在 GHC 中无法通过类型检查的表达式在 GHCi 环境中却可以通过,我们来看一个最简单的例子:

-- 在 ghci 中
λ> show $ reverse []
"[]"

-- 在普通的 ghc 中
foo = show $ reverse []
-- • Ambiguous type variable ‘a0’ arising from a use of ‘show’
--   prevents the constraint ‘(Show a0)’ from being solved.

在 GHCi 中,show $ reverse [] 返回了一个字符串 "[]",但在 GHC 中这个表达式不能过编译——正如错误信息所说,Show a 约束中的 a不确定 的,编译器无法从 [] 中推断出来,于是就不知道该选择哪个实例的 show 函数了。听起来很有说服力,因为 [a]a 的选择可能会改变 [a] 的显示方法:

data X = X
  deriving Show

instance {-# OVERLAPS #-} Show [X] where
  show [] = "??"
  show [x] = show x
  show (x:xs) = show x ++ ", " ++ show xs

-- >>> show ([] :: [X])
-- "??"

-- >>> show [X, X, X]
-- "X, X, X"

-- >>> show ([] :: String)
-- "\"\""

看来在 GHCi 中有特殊操作,帮助我们选择了一个 a 来显示空列表。让我们看看 GHCi 默认启用了哪些扩展:

λ> :showi language
with the following modifiers:
  -XNoDatatypeContexts
  -XExtendedDefaultRules
  -XNoMonomorphismRestriction
  -XNondecreasingIndentation

DatatypeContexts

NoDatatypeContexts…… 跑一下题,不得不说 DatatypeContexts 是个彻底失败的特性,来看个例子:

newtype Eq a => B a = B a

我们想让用来构造 Ba 必须满足 Eq 约束,好,咱来写个函数体验一下:

bEq :: B a -> B a -> Bool
bEq (B x) (B y) = x == y

-- • No instance for (Eq a) arising from a use of ‘B’
--   Possible fix:
--     add (Eq a) to the context of
--       the type signature for:
--         bEq :: forall a. B a -> B a -> Bool
-- • In the pattern: B x
--   In an equation for ‘bEq’: bEq (B x) (B y) = x == y

非常不幸,这是个伤星的故事——在模式匹配解构 B 时,它不光没有附带 a 满足 Eq 的证据,反倒向我们索要这个证据。在类型构造器前加的 Eq a => 只确保了在 构造 Ba 必须满足 Eq。于是我们需要在所有用到 B 的函数的签名上都加上 Eq a =>(如果不这样我们是没有办法解构 B 的)显然非常离谱。当然这个语言扩展已经将近废弃十年了,新的入门教程中几乎不会再出现它的身影,一些稍老的材料中也会提醒读者不要使用它。

ExtendedDefaultRules

能让开头 show $ reverse [] 在 GHCi 工作的正是这个语言扩展。这个扩展意为“扩展默认的规则”,那我们就得先搞明白“默认的规则”是啥。首先我们要知道,Haskell 中整数字面值的类型是 Num a => a,浮点数字面值的类型是 Fractional a => a。那么问题来了:233 == 233 中,233 的类型是啥?(==) :: Eq a => a -> a -> Bool,这和前面例子中的 Show 很相似——光有 Num a 约束我们是无法进行比较的,除非 Num a => a 被实例化成某个 具体 的类型。这里则有 233 :: Integer。为什么呢?在 Haskell 2010 Report 中规定:

  • 表达式 e 若有类型 forall U. cx => t,该类型的全称量化 U 中含有类型变量 uucx 中出现了但却没在 t 中出现,我们就说这个类型是 非法 的,而表达式 e 的类型是 不确定
  • 在模块顶层可以使用 default (t1 , … , tn) (n >= 0) 为该模块声明一个 默认规则
  • 当遇到 不确定 类型时,假设类型变量 v不确定 的,在以下条件满足时我们说 v可默认的
    • v 仅出现在一个约束 C v 中(C 是一个类型类)
    • 约束的类型类必须是 NumNum 的子类
    • 这些类型类必须定义在 Prelude 中
  • 可默认的 类型变量会被 默认规则 列表中第一个所满足约束的类型替代,如果没有这样的类型则产生错误
  • 每个模块仅能有一个 默认规则 声明,它的作用域是该模块;如果一个模块没有 默认规则 声明,那么使用 default (Integer, Double)
  • 为模块声明 default () 会禁用掉这个功能

不难理解 print 233233 == 233 这些时候都有 233 :: Integer。那么开启 ExtendedDefaultRules 之后会发生甚么事呢?根据 GHCi 文档,当遇到约束 (C1 a, C2 a, …, Cn a) 时:

  1. 找到所有未解析的约束
  2. C a 这样的形式,把未解析的约束进行分组,使得一个组内不同的 C 共享相同的 a
  3. 仅保留包含至少一个 C交互式类型类 的组
  4. 对于每个留下的组 G,尝试将定义在 默认规则 中的 ty 依次代入 a;如果这让 G 中所有的约束都能被解析了,那么 a可默认类型 就是 ty

此外,ExtendedDefaultRules 还会:

  • 定义 ShowEqOrdFoldableTraversable交互式类型类
  • 放宽限制——默认规则 中的类型必须要实现 Num 改为必须是 交互式类型类 的实例
  • 将标准情况下的 default (Integer, Double) 改为 default ((), [], Integer, Double)

按照这样的处理方式,只要 默认规则 中含有任意满足 Show 约束的类型,咱叫它 Fooshow $ reverse [] 就可以通过类型检查——列表的类型会被推断成 [Foo]

MonomorphismRestriction

这是个比较常见的坑,而且把每种情况解释一遍会很占篇幅,因此在这儿就说个大概。

上节我们提到过,print 233 中受到 默认规则 的影响 233 的类型是 Integer。那么在一个整数字面值的绑定中呢?

qwq = 233
-- Top-level binding with no type signature: qwq :: Integer

GHC 告诉我们 qwq 的类型是 Integer。可这里并没有涉及到使用约束,为什么字面值还是被实例化成了一个 具体 的类型呢?这就是 MonomorphismRestriction 的作用,没有类型签名的绑定会可能被应用 默认规则。这会让我们创建的绑定没有那么地“多态”。让我们一起看几个来自 Wiki 的例子:

f1 x = show x

f2 = \x -> show x

f3 :: (Show a) => a -> String
f3 = \x -> show x

f4 = show

f5 :: (Show a) => a -> String
f5 = show

默认不动任何扩展的情况下,f1f3 以及 f5 都是我们想要的,它们具有类型 Show a => a -> String;而 f2f4 无法通过类型检查,Show a 中的 a不确定 的。更有意思的是,这时候要是启用 ExtendedDefaultRulesf2f4 也是良型的了,但它们的类型是 () -> String。原因很简单,上节中我们提到过,() 会被插入到 默认规则 的头部,所以 GHC 选择了第一个能解析约束的 () 来实例化 a。当然,这有些反直觉——eta-reduce 一下居然改变了语义。根据 Haskell 2010 Report,这种限制是为了解决两个问题:无法通过标注类型签名解决的不确定性,以及不必要的重复求值。在 GHCi 中,我们通常不希望这种单态化发生:

λ> :set -XMonomorphismRestriction
λ> plus = (+)
λ> plus 233.3 3

<interactive>:28:6-10: error:
No instance for (Fractional Integer)
        arising from the literal ‘233.3
In the first argument of ‘plus’, namely ‘233.3
      In the expression: plus 233.3 3
      In an equation for ‘it’: it = plus 233.3 3

显然,这里 plus 的类型是 Integer -> Integer -> Integer。但在文件中用 GHC 编译是不会有问题的:

plus = (+)

qwq = plus 233.3 3

因为 GHC 会在推断出 plus 的类型前先看一看它被调用的地方,这时就有 plus :: Double -> Double -> Double;但在 GHCi 中语句是一步一步执行的,声明完 plus 直接就定下来了。

NondecreasingIndentation

咱也不知道这是干啥的,查了下好像在某些情况下可以让缩进往前越一级(?)不懂在 GHCi 里有啥用(x

GHCi 中的语句执行

在 GHCi session 中可以:

λ> let x = 1          -- 绑定纯的变量
λ> x' = 1             -- 绑定纯的变量,let 可以省略
λ> y <- pure 2        -- 绑定 IO 结果到变量
λ> 3 + 3              -- 求值表达式并打印
6
λ> print 4            -- 执行 IO 操作
4
λ> it                 -- 获得上次的求值结果
()

是不是感觉有点像在一个 IO () do notation 中,但可以省略 let、还多了个 it。这个 it 比较有意思,咱后面细说。咱可以直接看 GHCi 中的某个入口函数,它直接接收输入的字符串:

-- GHCi/UI.hs

runStmt :: GhciMonad m => String -> SingleStep -> m (Maybe GHC.ExecResult)
runStmt input step = do
  pflags <- initParserOpts <$> GHC.getInteractiveDynFlags
  st <- getGHCiState
  let source = progname st
  let line = line_number st

  -- 如果输入是一个 statement
  if | GHC.isStmt pflags input -> do
         hsc_env <- GHC.getSession
         -- 尝试 parse 它
         mb_stmt <- liftIO (runInteractiveHsc hsc_env (hscParseStmtWithLocation source line input))
         case mb_stmt of
           Nothing ->
             -- parse 失败什么也不做
             return (Just exec_complete)
           Just stmt ->
             -- 调用 run_stmt 执行
             run_stmt stmt
     -- 如果输入是一个 import
     | GHC.isImport pflags input -> do
         -- 添加 import
         addImportToContext input
         return (Just exec_complete)

     -- 其他情况我们把输入当作 declaration
     | otherwise -> do
         hsc_env <- GHC.getSession
         let !ic = hsc_IC hsc_env
         -- 尝试 parse 成 declaration
         decls <- liftIO (hscParseDeclsWithLocation hsc_env source line input)
         -- 看下个代码块
         run_decls decls

x = y 是一个声明(declaration),但 let x = y 是一个语句(statement)。GHCi 的处理非常直接,把所有这种声明 parse 完之后将 AST 改写成 let 语句并执行:

-- GHCi/UI.hs

run_decls :: GhciMonad m => [LHsDecl GhcPs] -> m (Maybe GHC.ExecResult)
run_decls [L l (ValD _ bind@FunBind{})] =
  run_stmt (mk_stmt (locA l) bind) -- 调用 run_stmt 执行
run_decls [L l (ValD _ bind@VarBind{})] =
  run_stmt (mk_stmt (locA l) bind) -- 调用 run_stmt 执行
run_decls decls = do -- 如果不是 FunBind 或者 VarBind,还得按声明来处理
  m_result <- GhciMonad.runDecls' decls
  forM m_result $ \result ->
    afterRunStmt (const True) (GHC.ExecComplete (Right result) 0)

-- 把 Bind 变成 LetStmt
mk_stmt :: SrcSpan -> HsBind GhcPs -> GhciLStmt GhcPs
mk_stmt loc bind = let la = ... in la (LetStmt noAnn (HsValBinds noAnn (ValBinds NoAnnSortKey (unitBag (la' bind)) [])))

可以看到“省略 let 的绑定”是在 GHCi 入口处实现的。那么 run_stmt 是啥?GHCi 最终调用的函数应该是 execStmt' ——这是 GHC 暴露 API 的一部分,所以有 Haddock 文档。这个函数干的最重要的一件事就是调用了 hscParsedStmt:而语句在这个函数中走过了完整的编译过程:tcRnStmt 完成 rename & typecheck、deSugarExpr 完成 desugar & generate core、hscCompileCoreExpr 完成 codegen & link。我们只关心类型检查部分。从注释我们可以清晰地知道 GHC 在类型检查时加 buff,实现创建交互式绑定以及 it 变量的策略:

                Typechecking Stmts in GHCi

Here is the grand plan, implemented in tcUserStmt

        What you type                   The IO [HValue] that hscStmt returns
        -------------                   ------------------------------------
        let pat = expr          ==>     let pat = expr in return [coerce HVal x, coerce HVal y, ...]
                                        bindings: [x,y,...]

        pat <- expr             ==>     expr >>= \ pat -> return [coerce HVal x, coerce HVal y, ...]
                                        bindings: [x,y,...]

        expr (of IO type)       ==>     expr >>= \ it -> return [coerce HVal it]
          [NB: result not printed]      bindings: [it]

        expr (of non-IO type,   ==>     let it = expr in print it >> return [coerce HVal it]
          result showable)              bindings: [it]

        expr (of non-IO type,
          result not showable)  ==>     error

HValue 相当于一个装着 Any 的容器——编译、求值一个表达式的结果可能有任意类型。GHC 将这几种情况分成三类,名曰 plans:

  • Plan A. [it <- e; print e]it 不能是 ()
  • Plan B. [it <- e]
  • Plan C. [let it = e; print it]

可在 tcUserStmt 中找到相应的代码:

-- TcRnDriver.hs

tcUserStmt :: GhciLStmt GhcPs -> TcM (PlanResult, FixityEnv)
tcUserStmt (dL->L loc (BodyStmt _ expr _ _))
  = do { (rn_expr, fvs) <- checkNoErrs (rnLExpr expr)
-- ...省略
          let
              -- [it = expr]
              the_bind  = cL loc $ (mkTopFunBind FromSource
                                     (cL loc fresh_it) matches)
                                         { fun_ext = fvs }

              -- [let it = expr]
              let_stmt  = cL loc $ LetStmt noExtField $ noLoc $ HsValBinds noExtField
                           $ XValBindsLR
                               (NValBinds [(NonRecursive,unitBag the_bind)] [])

              -- [it <- e]
              bind_stmt = cL loc $ BindStmt noExtField
                                       (cL loc (VarPat noExtField (cL loc fresh_it)))
                                       (nlHsApp ghciStep rn_expr)
                                       (mkRnSyntaxExpr bindIOName)
                                       noSyntaxExpr

              -- [; print it]
              print_it  = cL loc $ BodyStmt noExtField
                                           (nlHsApp (nlHsVar interPrintName)
                                           (nlHsVar fresh_it))
                                           (mkRnSyntaxExpr thenIOName)
                                                  noSyntaxExpr

              it_plans = [
                    -- Plan A
                    do { stuff@([it_id], _) <- tcGhciStmts [bind_stmt, print_it]
                       ; it_ty <- zonkTcType (idType it_id)
                       ; when (isUnitTy $ it_ty) failM -- it 不能是 ()
                       ; return stuff },

                        -- Plan B
                    tcGhciStmts [bind_stmt],

                        -- Plan C
                        -- 先看一看 let 绑定是不是良型的
                        -- 否则会得到两个错误信息,一个在 let 绑定上,一个在打印上
                    do { _ <- checkNoErrs (tcGhciStmts [let_stmt])
                       ; tcGhciStmts [let_stmt, print_it] } ]

-- ...省略

GHC 在这里加的特效大概就是拼接 renamed AST 来进行下一步的 tcGhciStmts

  • Plan A:e 是一个 IO 操作,并且这个操作的返回值可以打印并且不是 ()。将 e 的执行结果绑定到 it 上,并打印 it
  • Plan B:e 是一个 IO 操作,并且这个操作的返回值无法打印。将 e 的执行结果绑定到 it 上,不打印
  • Plan C:e 是一个可以打印的表达式。将 e 绑定到 it 上,并打印 it

从 Plan A 开始依次尝试,若全部失败就打印错误信息。这里的“打印”并不是使用 print,而是 ic_int_print。后者是一个任意具有类型 a -> IO () 的函数,可以通过 -interactive-print=<FUNCTION_NAME> 选项在 GHCi 启动时指定。

总结

到目前为止,相信读者已经对 GHCi 这套操作很熟悉了。那么回过头来看我在文章开头提出的困惑:

λ> :set -XNoExtendedDefaultRules
λ> head []
*** Exception: Prelude.head: empty list

为什么关闭了 ExtendedDefaultRules 之后 head [] 依然能在 GHCi 中预期执行,没有产生 a 不确定的错误呢?再看一眼上面的三个 plans —— Plan A 和 Plan C 都需要 Show 约束,只有 Plan B 不需要。答案呼之欲出:我们在尝试获取并执行(并非打印)一个 [IO a] 列表的头部,但这个列表是空的,所以扔出了错误,给我们造成了“正在尝试打印空列表头部”的假象。读完这篇文章可能会获得一些没什么用的知识:

  • headlast 这种 [a] -> a 的函数在 GHCi 中应用到空列表时没有用到 Show 约束
  • pure () 在 GHCi 中不会打印 ()pure 233 会打印 233
  • ……

April 11, 2021 11:58 PM

April 08, 2021

中文社区新闻

附带安装器的安装媒介

最新的安装媒介开始附带一个有指引的安装器
这是作为默认安装方式(跟随安装向导)的补充,比较像其余安装方式
如果你使用了这个安装器,不要忘记在寻求技术支持的时候提及这一点,以及如果被问到的话提供 archinstall 的日志。

by farseerfc at April 08, 2021 10:55 AM

March 30, 2021

frantic1048

春游武汉

最近被小伙伴安利到武汉的樱花,才知道原来还有这么厉害的地方,正好花季,找了个就近的周末,3 月 13 日就上路了。

spring in wuhan

上次去武汉还是 2016 年 8 月到 Deepin 实习的一阵子,时逢雨季涨水,去的又是被水淹特别厉害的金融港。刚到武汉的夜晚,司机开了大半程路说小伙子只能送你到这里了,前方在月光下荡漾的大池塘是地图上的公路,这还真是港口啊!每天上班十分钟步行路上很可能就突然来一波大风大雨,就算提前打伞也防不住一身的水,公司门口旁边就是个小浴室,不知道是不是也是为了应对这样的天气;一波下来的感受就是和传说中的「火炉」的外号一样,是个天气很猛的地方。

这次去天气就很舒服了,中午到武汉,吃了饭就立刻打车去东湖,结果先是一个司机各种不愿意去,第二个司机说你们这一趟少说两个小时,当时还疑惑这就几公里是要怎么走这么久的,就还是上车了。因为出发起得很早,车上困了一觉,醒来之后一看窗外,被车包围,再看地图,只走了一半多点的路程,卡在东湖隧道,时间已经下午三点多了,按照这速度四点能到都悬,结果出了隧道之后和小伙伴商量决定下车走过去了,还真比车走得快不少,到下午四点半终于到了东湖樱园门口,被人群吓到,差点没找到入口。进去之后还是人海,只是多了很多很多各种各样的大和更大的樱花树,非常密集,很棒。唯一的问题是,人太多了,和老家景区的黄金周完全一致,车行道堵车,人行道堵人,只要是能塞下人的地方都会有人,这方面体验很糟糕。可是的确又是得这个季节来才能看到花,体会了一番过去自己眼中的不嫌人多凑热闹傻子游客的心境。

来都来了,还是暂时忘掉人群,多关注一下花吧!

spring in wuhan spring in wuhan spring in wuhan spring in wuhan spring in wuhan

作为樱园,草坪、步行道、大灯我都懂,作为景区,各种小吃饮料和歇脚处也没毛病,到处汉服出租点也行吧有人喜欢,突然出现的小猪佩奇一家子还是超出了我的预想。

spring in wuhan

因为去的挺晚,很快就晚上了,顺便也感受了一下夜樱,各种 RGB 打上去还是蛮不错的。

spring in wuhan spring in wuhan

大门后面的樱园名字的石雕:

spring in wuhan

第一天路上想着有点晚要不要第二天早上再去一次东湖,路上堵了一波之后决定第二天还是去另一个也有很多樱花的解放公园逛逛。早上起来,一看地图东湖隧道就又堵上了。

spring in wuhan

出门沿着江边一路朝解放公园步行,路过一家星巴克,然后看到地图旁边是一家叫做 1.Z Coffee(江景店)的评价不错的店,想着来都来了,就上楼去感受了一下江景。

休息中的小伙伴: spring in wuhan

肥宅快乐三角: spring in wuhan

路上见到的大号加湿器: spring in wuhan

沿江一路步行,经过江滩公园,就接着逛了。

spring in wuhan spring in wuhan spring in wuhan spring in wuhan

一眼感觉是普通树林,仔细一看是光效打满的赛博树: spring in wuhan

神奇小店: spring in wuhan

终于到了解放公园,人也挺多,只是没东湖那么可怕了。

在公园里面又遇到了中华名塔园,进去康康。这里是个包含各地著名塔的缩小版石雕的园子,塔很多,细节也很不错,要是哪天能建一个 1:1 手办园就好了。

spring in wuhan spring in wuhan spring in wuhan spring in wuhan spring in wuhan

公园里另一处有个聚集非常多人的地方,是个有很多鸽子的木屋,看了一眼地图,这个地方叫做「鸽子广场」,我悟了! spring in wuhan

解放公园里各种花也蛮多的。

spring in wuhan spring in wuhan spring in wuhan

March 30, 2021 12:00 AM

March 25, 2021

ヨイツの賢狼ホロ

用 Hyper-V 玩些小花样

从前面那篇 Hyper-V 和 OpenWrt 搭建软路由 开始咱就开始在 Windows 上用 Hyper-V 了。 然后场面逐渐失控……

这篇文章就是把咱这几天遇到的各种问题和可能的解决方案记下来这个样子。

汝应该尝试 Hyper-V 吗?

如果汝有像咱一样的某些需求的话,那 Hyper-V 可能也很适合汝。

  • 想在 Windows 开机时自动启动某个虚拟机。

    VMware Workstation 15 还可以用内置的共享虚拟机设置开机启动的,不过 16 开始弃用了。 VirtualBox 文档里有提用服务设置开机启动虚拟机。 虽然咱一次都没成功过……

  • 有用另一台电脑访问这台电脑上虚拟机的需求。

  • 在用 WSL 2,那其实汝已经算是启用了 Hyper-V 了嘛。

  • (或者还有别的?)

不过汝也许会因为这些原因不想尝试。

  • 旧版 VMware Workstation 或 VirtualBox 用户。

    启用 Hyper-V 以后会导致旧版 VMware Workstation 或 VirtualBox 无法启动虚拟机。

    也有人觉得在开启 Hyper-V 的主机上开别的虚拟化软件时性能会下降什么的。

  • 想在 Windows 上试用 GNU/Linux 桌面。

    Hyper-V 的 framebuffer 缓存(是这个意思吧?)只有 8M,于是开 GNU/Linux 桌面可能会很吃力。 (增强会话是基于 RDP 的,嗯……)

  • Windows 10 家庭版用户 (也没法开)

以及咱这几天下来对 Hyper-V 的槽点,大概就只有那个检查点了吧, 虽然有类似快照的“检查点”,但是不能让某个虚拟磁盘不在检查点里。(VMware 的独立虚拟磁盘或者 VirtualBox 的完全写入)

环境准备

如果是 Windows 10 专业版(或者专业工作站版/企业版/教育版啥的),就参考前面那篇文章了。

不过咱这回换成 Windows Server 2019 了,所以从服务器管理器安装 Hyper-V 角色就可以了。

以及如果汝只需要 Hyper-V 这一个功能的话,还可以选择用 Hyper-V Server , 虽然这个没有图形界面,不过可以通过 Windows Admin Center 或者 Windows 远程管理来管理。

安装和使用 Windows Admin Center

Windows Admin Center 是本地部署的基于浏览器的应用,用于管理 Windows 服务器、 群集、超融合基础设施和 Windows 10 电脑。 它是免费产品,可供在生产中使用。

Windows Admin Center 概述 | Microsoft Docs

Windows Admin Center 可以装在 Windows 10 上作为客户端,也可以装在 Windows Server 上作为网络服务, 咱这次就装在 Server 上了。有桌面体验的话直接从 Microsoft Evaluation Center 下载和安装就好,如果是服务器核心模式或者 Hyper-V Server 的话,用 PowerShell 来下载和安装也是可以的。

# PowerShell 里也是有 wget 的,虽然用法和 GNU/Linux 里面的不太一样。
wget -Uri https://aka.ms/WACDownload -UseBasicParsing -OutFile c:\WindowsAdminCenter.msi
# 用 msiexec 安装 Windows Admin Center,这里选择用自生成的证书。
# SME_PORT 是 Windows Admin Center 的端口。
## 不要使用点斜杠相对路径表示法(例如 .\<WindowsAdminCenterInstallerName>.msi)
## 从 PowerShell 调用 msiexec。 该表示法不受支持,会导致安装失败。 请删除 .\ 前缀或指定 MSI 的完整路径。
msiexec /i c:\WindowsAdminCenter.msii /qn /L*v log.txt SME_PORT=443 SSL_CERTIFICATE_OPTION=generate

安装好后 Windows Admin Center 就会从 Windows Update 接收更新了。 然后从浏览器打开 Server 的 IP 地址:

Microsoft 推荐是用 Microsoft Edge 或者 Google Chrome 来访问 Windows Admin Center 啦。

在输入服务器上管理员的用户名和密码登录以后,就能见到主界面啦。

Windows Admin Center 的主界面

选择要连接的服务器,就能看到服务器的各种状态。

主机状态

如果已经安装好 Hyper-V 的话,就能在这里管理虚拟机。

虚拟机管理器

也可以管理设置和远程连接虚拟机。

虚拟机连接。

不过用 Windows Admin Center 下载来的远程桌面连接的时候,要输入的是服务器的用户名和密码的样子。

使用 Hyper-V 管理器和 Windows 远程管理(WinRM)管理虚拟机

效果就是可以用 Windows 上的 Hyper-V 管理器连接到服务器,然后就像 Hyper-V 运行在本地一样。 不过也挺麻烦的就是了。

首先,在服务器上启动 Windows 远程管理服务,以及适当的前期配置。 下面的命令都是在以管理员身份运行的 PowerShell 里完成的。

# 启动 WinRM 快速配置。
# 这三步中间的询问按提示来就可以了。
# 如果遇到因为公用网络无法设置防火墙规则的话,解决方法在下面。
winrm quickconfig
# 启用 PowerShell 远程访问。
Enable-PSRemoting
# 为凭据委派(咱也不知道这是啥)启用 CredSSP。
Enable-WSManCredSSP -Role server

如果汝遇上因为公用网络无法设置防火墙规则的话,可以用 PowerShell 更改网络类型。

# 用 Get-NetConnectionProfile 获得现在连接的网络适配器的信息
Get-NetConnectionProfile

    Name             : 网络 2
    InterfaceAlias   : vEthernet (Internal)
    InterfaceIndex   : 8
    NetworkCategory  : Private
    IPv4Connectivity : Internet
    IPv6Connectivity : LocalNetwork

    Name             : 网络
    InterfaceAlias   : vEthernet (External)
    InterfaceIndex   : 18
    NetworkCategory  : Public
    IPv4Connectivity : Internet
    IPv6Connectivity : Internet

# 用 Set-NetConnectionProfile 设置网络的类别,记得把 InterfaceIndex 换成汝需要更改的那个网络。
Set-NetConnectionProfile -InterfaceIndex 18 -NetworkCategory Private

接下来这些操作在客户端上完成。

# 首先就是把 Hyper-V 管理工具装上啦。
# 用 “启用或关闭 Windows 功能” 也是可以的。
add-windowsfeature rsat-hyper-v-tools
# 然后在客户端启动 WinRM 快速配置。
winrm quickconfig
# 设置客户端 WinRM 的信任的主机。
# 记得把 fqdn-of-hyper-v-host 换成服务器的完全限定域名(FQDN Hostname)
# 在 Windows 上的话, FQDN Hostname 就是服务器的计算机名加上连接特定的 DNS 后缀。
# 这个后缀能用 ipconfig 命令看到。例如咱这里的 FQDN Hostname 就是 "WIN-0JL0BB72KFC.lan"。
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "fqdn-of-hyper-v-host"
# 启用客户端 CredSSP 认证。
Enable-WSManCredSSP -Role client -DelegateComputer "fqdn-of-hyper-v-host"

接着打开组策略对象编辑器(gpedit.msc),把 "计算机配置 >管理模板 >系统 >凭据委派 >允许向仅 NTLM 服务器身份验证分配全新凭据" 设置为启用,并在列表里添加 “wsman/汝服务器的 FQDN Hostname” 。像这个样子。

设定必要的策略

打开 Hyper-V 管理器,选择“连接到服务器”,输入服务器的计算机名。以及把下面“以另一个用户身份登录”那项 选中。

设定服务器

不过设置用户的时候,这里的用户名是“服务器的计算机名\用户名”的形式。

设定服务器的用户

如果一切顺利的话,Hyper-V 管理器应该就能连接到服务器了。

连接成功的效果

通过 RemoteApp 运行 Windows 虚拟机上的应用

RemoteApp 就像 VMware 的 Unity 模式或者 VirtualBox 的无缝模式, 不过虚拟机不在运行 RemoteApp 的电脑上而是在远程连接上啦。

不过以官方的方法配置 RemoteApp 的话,汝需要设置远程桌面服务,购买相应的许可证,设置 Active Directory 等等操作。 咱这里就用一个第三方的开源小工具 RemoteApp Tool 好了。

可以在 https://github.com/kimmknight/remoteapptool/wiki/Windows-Compatibility 上看到兼容性列表, 基本就只有 Windows 的企业版和 Windows Server 支持了。(好像最新的 Windows 10 专业版也可以?)

安装好 RemoteAppTool 然后打开,界面大概就是这个样子。

RemoteApp Tool 的界面

点击下面的绿色加号按钮添加要供远程访问的应用。

RemoteApp Tool 添加应用

然后点击右下角的 “Create Client Connection” 生成远程桌面连接文件。基本上只要修改远程 桌面连接的地址就好。

RemoteApp Tool 生成 RDP 文件

然后就可以用远程桌面连接客户端连接啦,虽然可能没有本机上的无缝模式流畅。

RemoteApp 效果演示

以及 Windows 对远程会话的数量是有限制的(Windows 1 个,Windows Server 没有 RDS 许可时 2 个), 不过应该够用吧……


以上。

by ホロ at March 25, 2021 04:00 PM

March 16, 2021

frantic1048

FuRyu - 宇治松千夜 啦啦队

最近各种奇奇怪怪的事情忙,总算是有机会整理出这套千夜了,放在桌上每天都充满了精神!

顺便给博客糊了个勉强能点的分页,先将就着吧(

Chiya

Chiya Chiya

Chiya

Chiya Chiya

March 16, 2021 12:00 AM

March 14, 2021

ヨイツの賢狼ホロ

Hyper-V 和 OpenWrt 搭建软路由

笔记本上虚拟机越开越多,于是怎么给它们划分网络就是一个问题,例如代理软件什么的。

因为咱不想每一个虚拟机上就单独设置一遍,就想起来了咱给咱手上不支持代理设置的设备 (例如游戏主机什么的)连的那个运行有代理软件的路由器了。然后发现已经有人尝试过了,那就开工咯。

介于随便汝等说咱是更想授之以渔还是就是在偷懒都好的想法,咱不会给出事无巨细的操作方法。 当然如果汝也想跟着做的话,遇到问题时也可以来问咱(或者上网搜索一番)。

以及基于 Hyper-V 的特殊性,启用完以后可能会导致老的虚拟机软件无法启动。 嘛最新的 VMware Workstation 和 VirtualBox 应该是已经支持了的。

准备工作 - Hyper-V 管理器、映像和创建虚拟机

要使用 Hyper-V 的话,汝得有一个 Windows 8.1/10 专业版或者企业版, 或者 Windows Server 2012 以后的 Windows Server 才行。同时也要求汝的 CPU 支持必要的虚拟化技术。 (Intel VT-x 或者 AMD-V 什么的)可以通过 PowerShell 里的 systeminfo 命令确定是否可以开启 Hyper-V 。

在满足了前置要求之后,可以这样启用 Hyper-V:

# 如果提示找不到命令的话,换以管理员身份运行的 PowerShell 窗口。
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

或者用“启用或关闭 Windows 功能”也是可以的。

下载和转换 OpenWrt 映像

接下来到 OpenWrt 的网站上下载映像 (这时候最新的版本是 19.07,于是链接就是 https://downloads.openwrt.org/releases/19.07.7/targets/x86/64/

OpenWrt 下载页面的样子,这里咱们选择包含有内核和 rootfs 的 combined 映像。

这里咱们选择包含有内核和 rootfs 的 combined 那俩, 区别在于 squashfs 那个安装好以后系统分区像在普通路由器一样是只读的,可以实现像是升级或者复位等功能。 不过咱大概会用检查点来实现这些,那就直接用 ext4 的好了。下载和解压以后汝大概就可以得到一个像是 openwrt-19.07.7-x86-64-combined-ext4.img 这样的文件了。

但是 Hyper-V 只能用微软自己的 VHD 或者 VHDX 格式,所以还需要转换一下。 转换的方法有很多,咱这里给出咱的一个例子。

创建一个虚拟磁盘。这里用了 diskpart 命令。如果汝更偏好用磁盘管理操作的话, 可以参考 这篇文章

Microsoft DiskPart version 10.0.21332.1000
Copyright (C) Microsoft Corporation.
On computer: DESKTOP-H6MANBV

# 创建一个虚拟磁盘文件
# create vdisk file=<汝要存放虚拟磁盘文件的位置> maximum=<它的最大大小,以 MiB 为单位> type=expandable
# 假如汝有充足的硬盘空间的话,可以把上面的 type=expandable 改为 type=fixed 创建一个固定大小的虚拟磁盘。
# 可以提高一些虚拟磁盘的性能。当然动态扩展对 OpenWrt 来说也够用就是了……
DISKPART> create vdisk file=c:\test.vhd maximum=8192 type=expandable
    100 percent completed
DiskPart successfully created the virtual disk file.

# 选择刚才创建的虚拟磁盘
# select vdisk file=<汝要存放虚拟磁盘文件的位置>
DISKPART> select vdisk file=c:\test.vhd
DiskPart successfully selected the virtual disk file.

# 挂载刚才选择的虚拟磁盘。
DISKPART> attach vdisk
    100 percent completed
DiskPart successfully attached the virtual disk file.

# 初始化虚拟磁盘为 MBR 分区表。
# OpenWrt 官方编译的版本不支持 UEFI 启动,所以就用 MBR 了。
DISKPART> convert mbr
DiskPart successfully converted the selected disk to MBR format.

# 在下一步操作完成之后卸载虚拟磁盘
DISKPART> detach vdisk
DiskPart successfully detached the virtual disk file.

接下来把这个映像写入虚拟磁盘就 OK 啦。(以及咱发现 Rufus 能把虚拟磁盘识别出来)

Rufus 能给虚拟磁盘写入 RAW 映像。

创建虚拟交换机和虚拟机

有点像其它 Hypervisor 里的虚拟网络啦,虚拟交换机类型里的外部网络就相当于桥接, 内部就相当于仅主机网络。于是这里创建一个外部和一个内部交换机。

Hyper-V 里的虚拟交换机

留意外部交换机这里的“允许管理操作系统共享此网络适配器”这个选项,把这个选项取消选择以后, 这块网卡就只能由虚拟机访问了。等全部设置妥当以后,汝就可以把这个选项关掉让主机从虚拟机联网了。

Hyper-V 里的虚拟机代数,第一代是传统的 BIOS 引导

创建虚拟机的时候,因为刚才提起过的 OpenWrt 官方编译的版本不支持 UEFI 启动的问题,所以就用第一代了。

为虚拟机连接新的虚拟交换机

以及新建虚拟机的时候只能添加一个网络适配器,那这里就先把上面新建的内部交换机加上好了。

在虚拟机设置为虚拟机连接新的虚拟交换机

创建完成以后可以在虚拟机设置里添加新的网络适配器,虚拟硬盘就是刚刚转换好的磁盘映像(所以汝刚才卸载了没有?)。

在连接和启动虚拟机之后,汝大概就可以看到这样的界面:

OpenWrt x86 首次启动的界面

所以当然是先用 passwd 命令设置 root 用户的密码啦。

查看和调整网卡设定

如果汝像咱刚才一样先添加的内部交换机, ip link 命令应该可以看到虚拟机从上游获得了 IP 地址,也可以正常的联网。

(为了节省空间,这里省略了获得的和本地 IPv6 地址)

root@OpenWrt:~# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
        valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
        valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master br-lan state UP qlen 1000
    link/ether 00:15:5d:0a:a9:01 brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 00:15:5d:0a:a9:02 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.147/24 brd 192.168.10.255 scope global eth1
        valid_lft forever preferred_lft forever
4: br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether 00:15:5d:0a:a9:01 brd ff:ff:ff:ff:ff:ff
    inet 192.168.10.1/24 brd 192.168.10.255 scope global br-lan
        valid_lft forever preferred_lft forever

当然如果没有成功也别担心,记下界面名称(eth0 这样的)下面 link/ether 后面的文字, 这就是汝这个虚拟网卡的 MAC 地址啦。

在网络适配器的高级功能选项里查看虚拟网卡的 MAC 地址

然后在虚拟机设置里找到对应的适配器,高级设置里可以看到这个适配器的 MAC 地址。

于是在咱这里,要连到上游的接口就是 eth1 了。接下来用 vim 打开 /etc/config/network 文件。

它默认的里面只有 vim 让咱也很难办啊,如果汝对 VIM 的操作不是很熟练的话,记得准备一个操作指南在手边。
config interface 'loopback'
        option ifname 'lo'
        option proto 'static'
        option ipaddr '127.0.0.1'
        option netmask '255.0.0.0'

config globals 'globals'
        option ula_prefix 'fddb:38c5:4b0a::/48'

config interface 'lan'
        option type 'bridge'
        # 把这里的 eth0 换成汝内部交换价连接的适配器上的接口
        option ifname 'eth0'
        option proto 'static'
        # 默认是 192.168.1.1,如果担心和自己网络上已有的端冲突的话也可以修改一下。
        option ipaddr '192.168.50.1'
        option netmask '255.255.255.0'
        option ip6assign '60'

config interface 'wan'
        # 把这里的 eth1 换成汝外部交换价连接的适配器上的接口
        option ifname 'eth1'
        option proto 'dhcp'

# 如果汝的上游网络支持 IPv6,那可以添上这一段试试?
config interface 'wan6'
        option ifname 'eth1'
        option proto 'dhcpv6'
        option reqaddress 'try'
        option reqprefix 'auto'

保存完成以后可以重启网络服务,或者直接重启系统也行。一切顺利的话, 主机上的虚拟交换机应该能正确的获得 IP 地址:

Ethernet adapter vEthernet (Internal):

    Connection-specific DNS Suffix  . : lan
    IPv6 Address. . . . . . . . . . . : fd51:c6aa:1df:0:99e6:8496:55c7:733e
    IPv6 Address. . . . . . . . . . . : fddb:38c5:4b0a::a32
    IPv6 Address. . . . . . . . . . . : fddb:38c5:4b0a:0:99e6:8496:55c7:733e
    Temporary IPv6 Address. . . . . . : fd51:c6aa:1df:0:b04a:f9d5:61bd:63e4
    Temporary IPv6 Address. . . . . . : fddb:38c5:4b0a:0:8d12:c734:e2f0:8ec8
    Link-local IPv6 Address . . . . . : fe80::99e6:8496:55c7:733e%26
    IPv4 Address. . . . . . . . . . . : 192.168.50.145
    Subnet Mask . . . . . . . . . . . : 255.255.255.0
    Default Gateway . . . . . . . . . : fe80::215:5dff:fe0a:a901%26
                                        fe80::215:5dff:fe0a:a907%26
                                        192.168.50.1

接下来就像使用一个普通的有 OpenWrt 的路由器一样设置它吧,像是装上 luci 和代理软件什么的。

在所有的设置都妥当以后,除了上面提到的允许管理操作系统共享此网络适配器那个选项以外, 汝还可以考虑设置虚拟机开机启动。

(在“管理-自动启动操作”那里,可以设置成“总是”来让虚拟机在主机启动时自动启动。)

by ホロ at March 14, 2021 04:00 PM

March 10, 2021

ヨイツの賢狼ホロ

咱和()的 2020 年?

就不要 不合时宜的 问为啥隔了接近三个月才写了…… (?)

以及第四年的总结 在咱的 Matters 上。

为啥这一年只写了八篇文章?

啊这……咱写字的地方越来越分散了啊……

Google Search Console

Google Search Console 的表现,是怎么样呢?

这一年……?

因为众所周知的原因(COVID-19 啦……),于是几乎都是呆在家里, 然后被批判一番不务正业…… 那就记一下这一年玩过的游戏好了。

  • DJMax Respect V ,除了正式发售时突然降价引起抢先体验玩家不满和全程联网外加笨蛋反作弊插件以外, 只是 DJMax 这个老字号就能让咱接受前面的大部分缺点了。
  • 女神异闻录5 皇家版,给女神异闻录系列出加强版是 ATLUS 老传统了。
  • 集合啦!动物森友会,成功登上咱 Switch 游戏时间排行第一。
  • Project DIVA MEGA39s,咱为什么要买它…… FTDX 加上 DLC 它不香么……
  • Fluffy Store 和 ATRI -My Dear Moments- ,两部被推荐来看后觉得不错的视觉小说。(所以 fault - SILENCE THE PEDANT 还是没有出)
  • Untitled Goose Game,其实 2019 年在 PS4 上玩过了,今年借着在 Steam 上发售又玩了一遍。 (然而并没有人一起玩更新的双人模式……)
  • 妄想破绽和原神……一言难尽*2。
  • Halo: The Master Chief Collection,用手柄坑完了除了 ODST 以外的正常难度剧情,嗯…… 至少咱的老笔记本还跑得动。
  • 女神异闻录5 乱战 魅影攻手,第一次录下了全程 然后给友人做素材工具人(?)
  • Cyberpunk 2077,除了分不清是 Bug 还是剧情演出以外还好啦。 这话到底是赞美还是批评……
  • Spice & Wolf VR2, “什麼?你說今天12/10還有2077? 2077能有赫蘿嗎?” 新的动画鉴赏模式虽然短了点……
  • Stellaris, P社玩家没有一个无辜的……

当然除了这些以外还有些一直在玩的老游戏,也就是那些音游什么的。PSN 给咱发年度报告的时候去年游玩时间最长的 游戏还是 FTDX ……


剩下的一时想不起来了,就先这样吧。

by ホロ at March 10, 2021 04:00 PM

February 28, 2021

ヨイツの賢狼ホロ

Arch Linux 上的 GLNMP

前言

请允许咱引用一下 lilydjwg 的一段话:

我之所以现在记录这事儿,「现在」的原因是,我要在另一个系统上再测试一遍再发布出来, 「记录」的原因是,下一次我就不用想要执行哪些命令了。

所以这篇文章更多是留给咱自己看的,绝对不可能适合所有人(例如万一有人是为了塞给 V2Ray 一类的代理软件用什么的)。

记得 Read the Fine Manual 是汝等的好伙伴就好啦。

那为啥要叫 GLNMP ?

如果汝会这么问的话,那应该是有听说过 LAMP/LNMP 系统吧,大概就是 Linux,Apache(或者 Nginx),MySQL(现在可能更多在用 MariaDB) 和 PHP(还有可能是 Python 甚至 Perl?)的组合。因为除了此上下文环境中的“Linux”实际上指的是 GNU/Linux 操作系统。 所以咱就这么叫了。


接下来就从一个刚装好的 Arch Linux 开始了。

Nginx (先运行起来)

https://wiki.archlinux.org/index.php/Nginx

Nginx 官方有两个分支, mainline 和 stable。 看名字应该就能知道哪个更新哪个更稳定了。

安装好和启动服务之后,就可以打开 http://localhost 看看默认的主页了(就那个 Welcome to Nginx 啥的), 当然现在这些肯定不够,所以先把剩下的装上再回来设置了。

MariaDB

https://wiki.archlinux.org/index.php/MariaDB

在 MySQL 被 Orcale 收购之后,一些对 MySQL 的未来表示担心的人发起了 MariaDB 这个项目。到现在越来越多的 WebApp 和 GNU/Linux 发行版都开始(或已经)用 MariaDB 代替 MySQL 了。

在安装完 mariadb 以后记得初始化一下:

## 安装 MariaDB 的基础数据
# mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
## 启动服务,进行第一次启动的设置,除了设置密码那一步的话基本上都能保持默认。
## 像是禁用 root 用户的远程登录,移除默认的 annoymous 用户和移除测试用的数据库和权限啥的。
# systemctl start mariadb
# mysql_secure_installation
# systemctl restart mariadb

如果有需要的话,可以在 /etc/my.cnf.d/ 里修改适合自己的配置,例如:

# 让 mysqld 只使用本地 Unix Socket 连接。
[mysqld]
skip-networking

# 默认使用 utf8mb4 编码。
[client]
default-character-set = utf8mb4

[mysqld]
collation_server = utf8mb4_unicode_ci
character_set_server = utf8mb4

[mysql]
default-character-set = utf8mb4

调整好设置以后,可以用 mysql 命令来测试连接:

# mysql -u root -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 10.5.9-MariaDB Arch Linux

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

例如创建一个数据库和用户,然后为刚创建的用户分配刚创建的数据库的所有权限:

MariaDB> CREATE DATABASE mydb;
MariaDB> CREATE USER 'user'@'localhost' IDENTIFIED BY 'some_pass';
MariaDB> GRANT ALL PRIVILEGES ON mydb.* TO 'user'@'localhost';
MariaDB> FLUSH PRIVILEGES;
MariaDB> quit

PHP

https://wiki.archlinux.org/index.php/PHP

Arch 习惯是只留最新的版本的,除非官方仓库中别的包在过渡的时候。虽然其它版本也能从 AUR 里装, 但是没有保证就是了……

以及有一些扩展也在仓库里,看汝使用的应用有什么要求就装上吧。

然后就是修改 PHP 的配置文件 (/etc/php/php.ini) 和 PHP-FPM 的配置文件 (/etc/php-fpm.d/) 了,基本上就是修改下上传文件的大小,启用扩展和其它的别的。

Let's Encrypt

这都 1021 年了还需要人提醒给自己的网站上 TLS ?

咱是参考 通过 Cloudflare DNS 验证来申请 Let's Encrypt 证书 这篇文章用 Certbot 和 Cloudflare 来验证域名的,汝可能喜欢用像是 acme.sh 之类的别的方法的话,那照着 自己的喜好来的话也没有关系。

组合配置

DigitalOcean 有个 Nginx 配置文件生成器说不定有用: https://nginxconfig.io/

比较常见的组织 Nginx 配置文件的方法就是用不同的配置文件区分不同的网站,像 /etc/nginx/sites-available 和 /etc/nginx/sites-enabled 这样的。

咱的话,习惯把配置文件中 PHP 和 TLS 的配置再拆出来共用的样子。

PHP 的配置大概像这样:

# /etc/nginx/include/php.conf
location ~ \.php$ {
    # 404
    try_files $fastcgi_script_name =404;

    # default fastcgi_params
    include fastcgi_params;

    # fastcgi settings
    fastcgi_pass                    unix:/run/php-fpm/php-fpm.sock;
    fastcgi_index                   index.php;
    fastcgi_buffers                 8 16k;
    fastcgi_buffer_size             32k;

    # fastcgi params
    fastcgi_param DOCUMENT_ROOT     $realpath_root;
    fastcgi_param SCRIPT_FILENAME   $realpath_root$fastcgi_script_name;
    #fastcgi_param PHP_ADMIN_VALUE  "open_basedir=$base/:/usr/lib/php/:/tmp/";
}

TLS 的配置大概像这样,推荐参考 MoillaWiki:Server Side TLS ,Mozilla 也有提供一个配置文件生成器的样子,咱目前使用的配置有些差别。

# /etc/nginx/include/ssl.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout  10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options sameorigin;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "no-referrer";
server_tokens off;
#add_header Content-Security-Policy "default-src https: ; object-src 'none'";
#add_header X-Robots-Tag "none" ;
#add_header X-Download-Options "noopen";
#add_header X-Permitted-Cross-Domain-Policies "none";

组合在一起的其中一个例子:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name $_server;
    include /etc/nginx/include/ssl.conf;
    ssl_certificate /etc/letsencrypt/live/$_server/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/$_server/privkey.pem;

    root /home/horo/www/$_server;
}

在大部分都安排妥当以后,就可以设置让必要的服务在开机时自动启动啦。


差点就忘了 reStructuredText 怎么写…… ,虽然 Pelican 是有 markdown 支持的。

by ホロ at February 28, 2021 04:00 PM

February 22, 2021

中文社区新闻

mkinitcpio 将迁移到默认使用 Zstandard 压缩镜像

随着 linux-lts 升级到了 5.10 版本,现在 Arch Linux 的所有官方内核都支持了 zstd 压缩的 initramfs 镜像,所以 mkinitcpio 从版本 30 起将默认使用 zstd 压缩镜像,现在这个版本已经在 [testing] 仓库中。

如果,因为任何理由,你仍在使用 5.9 之前的内核版本,请确保修改 mkinitcpio.conf 中的 COMPRESSION 使用某个受内核支持的压缩方式,比如 gzip ,否则将 不能 引导进 mkinitcpio 创建的新版镜像中。

by farseerfc at February 22, 2021 06:03 AM

February 19, 2021

Justin Wong

Fit an Overfit with MegEngine

本文为MegEngine系列开篇,难得有机会写一些能公开发出来,又和自己平时工作高度相关的内容,期望下次更新不要再鸽一年了。

作为开篇,我会写一点点深度学习炼丹的基本概念,然后讲一个有意思的小实验: 如何用 MegEngine 拟合一个 “OVERFIT” 出来。

February 19, 2021 12:00 AM

February 18, 2021

Alynx Zhou

运行在 JACK 上层的 PulseAudio

很多朋友都知道我除了是个程序员以外还是个乐器玩家,很久之前因为想要录音上网了解了一下需要购买专门的麦克风声卡从此掉进深坑一发不可收拾。当然 Linux 用户在购买硬件之前需要做好功课,大概 16 年左右我还在上高中的时候用我还凑合的英语水平翻了几个 Linux 音乐论坛最后决定买一台 Focusrite Scarlett 2i4(不过它后来似乎更新了几个版本所以我购买的变成了 1st Gen),我不太清楚除了更换了接口之后还有什么奇怪的改动没有,所以这里就不盲目推荐大家买更新的型号了,反正声卡这玩意够用的话也不太需要追新。

做功课的时候还了解到常见的 Linux 桌面采用的都是 PulseAudio,但专业录音为了追求更低的延迟所以都使用 JACK,于是简单学习了一下,发现只要打开 Qjackctl 选择设备然后启动,Audacity 就可以选择使用 JACK 设备了。系统的其它软件仍然通过 PulseAudio 输出到板载声卡,不会冲突,基本满足我的需求也就没研究过其它的。

但是最近直播的时候总有人说我的耳机麦克风不太灵敏,我想了想不如干脆把录音用的声卡和话筒利用上,整个系统直接采用 2i4 作为默认声卡?但是虽然 PulseAudio 可以直接控制 2i4,软件的兼容性却不太好,比如 Audacity 启动的时候即使没有运行 JACK 它似乎也会尝试通过 JACK 连接 2i4 于是导致缓慢的启动和几下破音。于是只能采用网上常见的方案也就是把 PulseAudio 的音频输出到 JACK,但我觉得其实这样不太准确因为音频是既有输出又有输入的,所以我的标题是把 PulseAudio 运行在 JACK 上层。当然还有一种方案是采用 PipeWire,打算自己替代 PulseAudio 和 JACK 一桶浆糊的新项目,它欺骗程序让它们以为它就是 JACK 和 PulseAudio,但虽然我是个 GNOME Dev,我对这个不太感冒。有个常见的笑话是“现在有 N 个不那么好用的系统了,我们写个新的把它们全部替代掉!然后现在有 N + 1 个不那么好用的系统了!”。JACK 对于专业用户来说很好用,而专业用户通常是不太喜欢变化的,所以我不太期待 PipeWire 替代 JACK。

有人说 Linux 的音频系统比意大利面条还复杂,这倒不能说错,因为假如你看维基百科上那张巨恐怖的图的话确实是这样。但本质上来说音频不过是把信号丢给声卡,所以只要是个能写声卡的软件都可以叫音频系统嘛,也就不奇怪那张图那么复杂,实际上在现代的 Linux 桌面通常都集成 PulseAudio,我们也没必要去研究那些边边角角的奇怪方案,于是整个结构其实很清晰,一般的用户看到的都是这样的:

typical-desktop

然后作为对比,我之前的方式是这样的:

mine-old

而这篇文章要达到的目的则是下图这样:

mine-new

当然实际上假如你理解了这个结构的话,其实也没必要只用一个声卡,完全可以用独立声卡录音用板载声卡输出,只是 JACK 是绑定独立声卡的,于是就像下图这样:

some-interesting-things

总之这样做的依据在于 JACK 被我们绑定了独立声卡,然后利用 PulseAudio 设置不同的输入输出设备控制基于 PulseAudio 的桌面程序的输入输出,同时它还可以把自己作为 JACK 的客户端。而具体到与声卡交互,则全部都是内核里面的 ALSA 组件控制的。这里没有涉及到使用 ALSA 用户态组件的客户端程序,因为 PulseAudio 会把自己伪装成 ALSA 的客户态组件,于是这些老旧的程序就被连接到了 PulseAudio 上面从而无法直接占据声卡了。

了解原理之后就可以具体操作,首先需要安装 jack2,这个软件包包含的是 JACK 的组件,必须要装 jack2 因为 jack1 不支持 DBus 所以也就没办法和桌面交互了。安装 qjackctl 来控制 JACK,然后安装 pulseaudio-jack,这是让 PulseAudio 作为 JACK 客户端的兼容层。

然后需要启动 Qjackctl,在 Setup 的 Settings 选项卡里面选择 Advanced,将 Input Device 和 Output Device 全都设置为 2i4(应该就是 hw:USB 那一项),然后切换到 Misc 选项卡,像下图那样设置 Others 部分(基本就是除了 Keep child windows always on top 的全都勾上),这样就可以做到毫无感觉的启动 JACK(只要打开 Qjackctl 它就在后台启动 JACK,退出 Qjackctl 也只是退到后台)。

misc-tab

这样再次启动 Qjackctl,应该 JACK 就已经在控制 2i4 了,PulseAudio 应该会自动把 2i4 的控制权交给 JACK,省了不少事情。对于一些原生支持 JACK 的客户端程序来说这已经足够了,比如 Ardour 或者 Audacity,它们不经过 PulseAudio,直接连接到 JACK。接下来需要调整的是那些基于 PulseAudio 的客户端程序。

然后打开你桌面环境的音频控制器,比如我是 GNOME 就是 GNOME Control Center 里面的 Sound 选项卡,这里基本都是集成的 PulseAudio 控制,把 Input Device 设置成 Jack Source,于是 2i4 上的麦克风的录音便通过 JACK 传到 PulseAudio 再传到 PulseAudio 的客户端程序比如 OBS Studio 和 Firefox。然后如果你想让 PulseAudio 的客户端程序把音频输出到 2i4 上的耳机里面,那就将 Output Device 设置为 Jack sink,这样其实 PulseAudio 就是运行在 JACK 上层。

最后你需要设置 JACK 在登录时启动,这样 PulseAudio 才能找到 JACK,这个很简单,因为我们已经设置 Qjackctl 无感启动 JACK 了,那只要将 Qjackctl 设置为自动启动即可,比如 GNOME 用户可以在 GNOME Tweaks 里面设置。

当然,一般的家庭录音用户都是单声道麦克风,某些客户端程序需要自己手动设置,比如 OBS Studio 需要在 Advanced Sound Properties 里面勾选 Mono。

如果你遇到了一些奇怪的明明已经设置 PulseAudio 重定向到 JACK 却没有声音的情况,可能是因为你某些软件或者插件带了奇怪的设置,建议先重置它们试试。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at February 18, 2021 02:51 AM

February 05, 2021

中文社区新闻

从三月初起 Chromium 将失去同步支持

Google 已经发出通知说他们将从3月15日起禁止除了 Chrome 以外的所有浏览器访问 Google 的一些功能(比如 Google sync)。这一来自 Google 的变化将较早影响 Arch 的 chromium 包,从3月2日起,预计会在 Chromium 89 发布之后。

我们已经确定 数据同步(data sync) 会停止工作(密码、书签、等)。其它特性比如定位(geolocation)或者增强的拼写检查(enhanced spell check)可能可以继续工作一段时间。访问 Google Drive 的浏览器扩展也可能受影响,以及 LibreOffice 可能将失去存储文档到那儿(Google Drive)的能力。

其他发行版比如 openSUSE 和 Fedora 已经在他们的 Chromium 88 软件包中移除了将要被限制功能的 API key 。Fedora 的升级建议中详细描述了关于这个变化的观点,我还发现 Hackaday 的 这篇文章也可供参考。

by farseerfc at February 05, 2021 12:29 AM

February 03, 2021

Phoenix Nemo

在 Linux 系统中升级超微 BIOS 固件

最近突然有客户找来说 BIOS 进不去了呀…看截图 stuck 在 POST Code AB 大概就知道什么情况了。

这不就是经典的 y2k bug 再现嘛…

一般情况下,升级超微 BIOS 固件的推荐方式是制作 DOS 启动盘引导系统升级,不过现在 BIOS 进不去也没有物理 access 就只好在 Linux 里操作啦。

获取设备信息

简单命令 lshw

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
~> lshw | head -n 25
localhost
description: System
product: X9SCL/X9SCM (To be filled by O.E.M.)
vendor: Supermicro
version: 0123456789
serial: 0123456789
width: 64 bits
capabilities: smbios-2.7 dmi-2.7 smp vsyscall32
configuration: boot=normal chassis=desktop family=To be filled by O.E.M. sku=To be filled by O.E.M. uuid=[redacted]
*-core
description: Motherboard
product: X9SCL/X9SCM
vendor: Supermicro
physical id: 0
version: 1.11A
serial: [redacted]
slot: To be filled by O.E.M.
*-firmware
description: BIOS
vendor: American Megatrends Inc.
physical id: 0
version: 1.1a
date: 09/28/2011
size: 64KiB
capacity: 8128KiB

X9 主板不是 2015 年就 EOL 了吗这可真是够老了…

总之主板型号是 X9SCM 于是在超微找到了新版的 BIOS 固件。下载到系统中解压得到一堆文件,其中 X9SCM1.106 这个文件就是需要的 BIOS 固件本体。

编译 SUM 内核模块

Supermicro Update Manager (SUM) 是用于在系统中控制 BIOS/BMC 的程序。首先下载并解压,得到 sum 二进制文件和一堆其他东西。在 driver 目录中发现了对应发行版预编译的内核模块,但是直接 insmod sum_bios.ko 出错,好在它也提供了源码,那么就直接编译吧。

首先安装对应内核的 Linux 头文件,搜索 linux-headers 一般都可以找到。

1
2
~> uname -a
Linux localhost 4.19.0-13-amd64 #1 SMP Debian 4.19.160-2 (2020-11-28) x86_64 GNU/Linux

然后

1
~> apt install linux-headers-4.19.0-13-amd64 build-essential # 还需要 make 和 gcc

进入 driver/Source/Linux 执行 make,如果成功编译,则 insmod ./sum_bios.ko

嗯,这次没有报错了。

升级 BIOS

升级命令和其他主板商以及 RH 系的包命名风格一样喜欢大小写混合 = =…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~> ./sum -c UpdateBios --file ../X9SCM1.106/X9SCM1.106
Supermicro Update Manager (for UEFI BIOS) 2.5.1 (2020/11/12) (x86_64)
Copyright(C) 2013-2020 Super Micro Computer, Inc. All rights reserved.

WARNING: BIOS setting will be reset without option --preserve_setting
Reading BIOS flash ..................... (100%)
Writing BIOS flash ..................... (100%)
Verifying BIOS flash ................... (100%)
Checking ME Firmware ...
Putting ME data to BIOS ................ (100%)
Writing ME region in BIOS flash ...
- Update success for /FDT!!
- Updated Recovery Loader to OPRx
- Updated FPT, MFSB, FTPR and MFS
- ME Entire Image done
WARNING:Must power cycle or restart the system for the changes to take effect!

至此就基本完成啦。然后直接重启即可生效。

不过还是需要注意一下,升级固件都是有变砖风险的,某些情况下需要先联系硬件厂商支持确定升级路线。

以及新版 X10 开始升级 BIOS 需要激活 license 了… 嘛。听说超微的密钥早就被提取出来写了算号器了(什

最后的最后,提醒各位到 2038 年只有 17 年了哦(笑

最近突然有客户找来说 BIOS 进不去了呀…看截图 stuck 在 POST Code AB 大概就知道什么情况了。

这不就是经典的 y2k bug 再现嘛…

February 03, 2021 03:05 AM

January 29, 2021

中文社区新闻

开始提供 PHP 8.0 和 PHP7 旧包

php 包已经升级到8.0 版本。请参考上游提供的迁移指引。由于许多程序还尚未兼容 PHP 8 ,我们同时也提供了php7包,可以和第8版同时安装。依赖 PHP 的包已经根据这个更新做了相应调整,如果需要的话会依赖 php7 。您可能需要手动更新您的配置文件。 PHP7 的二进制和配置文件会有“7”的后缀:

  • /usr/bin/php -> /usr/bin/php7
  • /etc/php -> /etc/php7
  • /usr/bin/php-fpm -> /usr/bin/php-fpm7
  • /usr/lib/systemd/system/php-fpm.service -> /usr/lib/systemd/system/php-fpm7.service
  • /run/php-fpm -> /run/php-fpm7

我们也提供了一些为 PHP 7 打包的第三方模块:

注意对 php7 的支持非常有限,并且很可能在一年左右之后放弃对 php7 的支持,具体取决于大部分软件对 8 的兼容性。

by farseerfc at January 29, 2021 01:01 AM

January 15, 2021

Alynx Zhou

装机小记

由于笔记本散热和性能实在是不适合打游戏(有一说一,Optimus 双显卡还是没有直接单卡来得爽),所以很早就想组装一台台式机。特别是最近一直和高中同桌玩 Dota2,我的笔记本如果直播 Dota2,直播推流就会十分卡顿,而我又不想像 CSGO 一样降低画质玩。

作为计算机专业的学生,当然不打算直接淘宝整机啦。经过一番挑选和参考别人意见,最后成套配置是下面这样:

  • CPU:AMD Ryzen 7 5800X 3199(之前是 AMD Ryzen 5 3600X)
  • 主板:ROG STRIX X570-E GAMING 1999(特价)(之前是 微星 X570-A PRO 套装 2349)
  • 显卡:NVIDIA GeForce RTX 2070 SUPER 4300(之前是 七彩虹 RTX 2060 战斧 6G 2199)
  • 显卡支架:酷冷至尊 显卡支撑架 70
  • 内存:铂胜 C9BJZ 颗粒 白马甲 DDR4 3000 8G x4 1000
  • 网卡:Intel AX200(主板内置,之前是单独购买的花了 150)
  • 固态硬盘:三星 970 EVO Plus 512G 769
  • 固态硬盘:英睿达 MX500 2TB 1299
  • 机械硬盘:东芝 P300 2TB 400
  • 机械硬盘:西数 紫盘 2TB 400
  • 电源:海韵 CORE GX650 650W 金牌 549(特价)
  • CPU 散热:利民 AK120 145(之前是 超频三 东海x5 89)
  • M.2 散热:主板自带(之前是利民 M.2 硬盘散热片 39)
  • 机箱:Corsair 4000D 白色 629 (之前是 先马 鲁班1 209)
  • 机箱风扇:利民 TL-C12R-S x2 + 利民 TL-C12S 418(TL-C12R-S 只有 299 的三联包)(之前是 Arctic F12 PWM 温控 x3 120)

总计 15177。(有些具体价格我记不清了,而且京东价格总是波动。)性价比一般,因为我在其它人觉得没必要的地方分了很多预算比如主板风扇和硬盘,选 B450M 和西数的 NVMe 以及随便买个不带温控但是带灯的风扇的大有人在。

选这样一套配置其实有原因,可能很多人觉得为什么要给 R5 配 X570 这种高端主板,不如换成 B450 然后把预算加到别的上面。但按照我朋友的使用经验,4 代和以前的 AMD 芯片组存在 USB 兼容性问题,而且我装好机器后就在 Arch Linux CN 群里看到有人遇到这种问题,5 代似乎重新设计了 USB 控制器,目前一切正常。虽然这大概是最丐的 X570,连前面板 USB C 的接口都不支持,但是带 3900X 以下的 CPU 还是没什么压力的,而且我的机箱也没前面板 USB C。唯一的问题是微星不太厚道,今年 B550 出来之后又搞了个 X570 Tomahawk,大概只贵了几百块但是却有比肩旗舰的供电能力和前面板 USB C 支持,所以不再推荐购买 X570-A PRO 和 X570 Gaming Edge 这两个低端款了。有这种好东西为什么不一开始就吐出来???


更新(2020 年 11 月 16 日):AMD 发布了 Zen 3 系列的 CPU,游戏性能全面反超 Intel,对于我这种玩的都是吃 CPU 的游戏的用户来说实在是太香了,而且我一直想要 8 核心的 R7,于是买了首发的 R7-5800X,果然性能提升明显。然后正好有朋友的朋友打算装机买我换下来的 CPU 和主板,于是双十一为了 更好的 RGB 效果 更好的供电支持换了 ROG 的 X570-E(东哥疯狂耍猴,本来我是想 Tomahawk 的,但是微星的几款真香主板都无货,而且我对微星土里土气的设计也审美疲劳了,1999 的打人国度带眼睛它不香吗)。


如果仅仅是打游戏也可以把 R5-3600X + X570 换成 i5-9600KF + Z390,大概可以便宜一点然后升级一下显卡?我觉得差价是不够 2060 换 2060S 的。或者如果要求不高换成 R5-3600 也行。但 R5-3600(X) 是 12 线程而 i5-9600KF 只有 6 线程,所以我还是选择了 Ryzen,反正 AMD CPU 最近表现都很不错,没必要给 Intel 掏更多的钱。如果你在京东买,建议自己翻翻店铺,一般买主板 CPU 套装更便宜,但是它不会摆出来……

显卡一开始我是打算买个 1999 的 GTX 1660Ti 的,反正我玩的游戏也不需要光线追踪(难道真相不是本来就没几个游戏支持光线追踪嘛?),然而看到七彩虹最便宜的 2060 正在特价 2199,果断少买一条内存,加钱上这个。反正 1999 的 1660Ti 也是便宜货,而且看了一下视频似乎这块卡也不是丐中丐中丐,质量还可以(同价位索泰铭瑄也不能说是什么高端货吧?),反正内存可以再插新的,而显卡二手不值钱。什么你说 A 卡?虽然 A 卡对于 Linux 的驱动支持更好一点,但是 N 卡也不是不能用,而且我要开直播的话 NVENC 编码挺香的,而且对我的 Steam 库存来说 N 卡也更友好一点(但是骂老黄还是要骂的)。另外虽然我不做机器学习,但万一室友毕设需要的话,反正我上班也不用台式机,装个 CUDA 让他 SSH 上去用岂不是你好我好大家好。

内存其实没什么特别要求,不过由于 Ryzen 的设计,频率越高越好,但按照这一代的设计,最佳频率是 3600 左右,再高反而会下降,我是买不起那么奢侈的内存,甚至 3200 的都买不起,但这款 3000 的也不错,据说颗粒是镁光创下超频记录的那一批,所以就买了四条。我也不需要 RGB,这款的马甲还挺好看的。当然 3000 是 XMP 频率,需要在 BIOS 里打开 XMP,不然默认只有 2400。最近抄了个 3600 16-19-19-36 的作业,结果四条内存轻轻松松就上去了,2K 下面 PUBG 大概提升了 20 FPS,还是挺爽的。

硬盘不多说了,买 MX500 还是看中一个稳,我还是不放心买同价位国产白片……而且作为一个 2016 年就在笔记本上使用 NVMe SSD 的人,表示并没感觉出 NVME 和 SATA 有什么使用差别……我也不渲染视频,写写代码打打游戏都不卡。但是后来打脸了,公司发了新年福利于是还是上了个 NVMe 硬盘。顺便还配了个无线网卡用来接蓝牙和 WiFi,似乎 Intel AX200 是对 Linux 支持比较好的,就随便买了个 PCI-E 的插上了(御三家集齐了!)(ATX 的好处终于用上了,多出来的 PCI-E 随便插)。

电源有点买亏了,买完了发现长城同系列 550W 的电源当时也是 329,都怪京东迷一样的定价策略,好在我这一套功耗并不是很高,当然最后趁着福利 + 打折还是换成了海韵一元一瓦的金牌全模组,带我这套绰绰有余。开始我听说原装散热器也能压住 3600X,所以就没打算换,但后来发现刀法还是精准啊,3600X 带的散热器竟然不是铜芯的,再加上 Ryzen 三代的电压控制比较激进,待机温度有时候超过 50,打游戏时候机箱上方有点烤腿,所以还是换了个塔式散热器,风道科学了不少,温度控制也更好了。而且 AMD 原装风扇转速太高,3000rpm 的时候机箱都在震,换了之后安静了很多。顺便安原装散热器时候拆下来的螺丝卡扣不要扔,万一换塔式散热器,好多都是需要装在这个卡扣上的。

说到机箱和风扇我就一肚子气!本着对京东自营品质和速度的信赖,我全套都是在京东买的,结果拿到手全部安装上之后发现机箱开机跳线是短路的,插上电源就开机,开一会因为短路主板以为你在长按电源又关了!开始我还以为是主板坏了,结果发现螺丝刀手动碰一下开机跳线开关是可以正常开机的!于是又费了九牛二虎之力拆下来退货,主板散热器显卡都在盒子里椅子上放了两天,还好新机箱没问题,但是我买风扇的时候又给我发了个断轴的!东哥呀东哥,我拿你当兄弟,你拿我当代价?你是盯上我了?虽然我平时说你两句坏话但是对京东的服务还是好评的,但是经过这次之后我还是得重新考虑考虑了……顺便据说启航者 S5 这个机箱前面板音频口有的是 HD Audio 有的是老版 AC97,涉嫌虚假宣传,如果你遇到 AC97 的大概可以换货,麻烦一点,不过反正便宜货就这样子,我的抽奖抽到 HD Audio 了。但是这个机箱设计的很抠门,比如主板装上了就没法在上面走 CPU 电源线了,有两个走线口直接被 ATX 主板盖住,所以主板和显卡的电源线只能从一个口里挤出去,然后固态硬盘就在这个口下面所以也很难接线……而且机箱侧板是个黑色半透明的亚克力(那就别宣传透明啊!),金属外壳感觉也不是很厚实。所以最后还是换了先马的鲁班 1,各种设计都宽松了许多,装起来也很好看。

其他的外设我自己都有,显示器 就是之前买的优派 XG2402,1080p@144Hz 并且自带扬声器 换成了 DELL S2721DGF,27 寸的 2K@165Hz 屏幕,除了通病漏光以外都还不错,音箱外接了一个 JBL 的蜗牛一代(因为我觉得二代没有一代好看),鼠标就卓威 EC1-B CSGO 特别版(这个版本已经停产了,而且有偶尔指天/指地的 bug,建议买新的 EC1/2 或者 DIVINA 版本),键盘则是前段时间买的 ikbc C87 红轴,便宜还好用。

顺便由于我手残以及力气小和室友跃跃欲试,很多东西都是他装的,非常感谢。话说回来装这东西还真是个力气活,毕竟接口都有防呆设计仔细看看不会装错,但是真的很紧很难拔……非常担心把主板搞坏了。


更新:

装好的完全体照片


更新:避免你们说我灵魂走线,重新整理了一下,线太硬了。

重新走线正面

重新走线背面


更新:内存插满。

内存插满


更新:NVMe + 蓝牙无线网卡的完全体。

完全体

这个机箱 CPU 线走上面是要把主板拿下来才能穿过去的,而且右边两个有硅胶垫的孔 ATX 完全不能用,只能用一个孔,而且不能把线固定在机箱中间,很难盖上。


再更新:新机箱比原来的好看多了也宽敞多了。

正面

背面

公司的蜥蜴(明明是变色龙!)玩偶太高了,显卡下面放不开,挂着我又不放心,盖盖子之前还是拿出来了。


更新:换上了公版 2070 SUPER 和利民 AK120。

显卡

全景


更新(2020 年 10 月 12 日):冬天到了,还是换了几个支持 ARGB 的风扇,我个人不喜欢蓝光紫光夜店土嗨风,所以就弄了点温暖的颜色假装是个电暖气。前面是利民 TL-C12R-S,后面是利民 TL-C12S,虽然是三联包,但是似乎螺丝有问题,有一个风扇螺纹被拧花了,而且后面的风扇竟然少一个角上的橡胶减震垫,于是我就没装拧花的那个,并且把它的减震垫安装到后面了。反正考虑到我最下面是机械硬盘,装上这个风扇风道也不畅通,而且那里并没有什么需要散热的设备,电源风道是独立的。每把风扇两根线,ARGB 线要串联,PWM 线要一分三,而且有前有后,风扇线还有编织保护套,还要防止线材打到风扇扇叶。理线花了好久,最后 把他们用扎带固定到下面和前面空的风扇挂架上 还是走背线了,刚好够长。而且我还有三个 SATA 硬盘打算换一下 SATA 线……我真的想不出来那些水冷排且上下左右全都是 ARGB 风扇的人怎么理线的。

正面看灯光

侧面看灯光

风扇线不在下面啦

最终的背线效果

我不会告诉你其实我是想调出我 TB 至宝的颜色:

TB 至宝

一开始我以为需要用 OpenRGB 这个项目才能在 Linux 下控制颜色,但是这种 ARGB 风扇好像有存储机制,会自动记住上次的设置。于是就在 Windows 下面安装了一个有一大堆乱七八糟组件和功能的 MSI Dragon Center,其实我只需要调成长亮,然后重启进 Linux 颜色就一直是我设置的,然后我想关掉 Dragon Center 的自启动因为反正也用不到,但是微星的软件自己拉跨,重启进 Linux 灯光不变,重启进 Windows 又变呼吸彩虹灯光了。最后发现 Dragon Center 里面有一个类似“覆盖第三方RGB软件”的选项……好像他把他自己上次存储的结果也当第三方软件了,关掉就好了。原理我猜因为 Dragon Center 只是个 Client,真正控制颜色的是他某个 SDK 里面的 Daemon,这个选项的意思其实就是 Daemon 每次启动都按照 Dragon Center 设置的颜色重新设置风扇就实现覆盖功能了……但是 Dragon Center 的启动被我关了所以就默认了,不管了,统统关掉就 OK。


更新(2020 年 11 月 16 日):有了打人国度的眼睛感觉瞬间高贵了起来(大误)。

老黄卡只有绿色一种颜色,于是只能整个机箱调个老黄绿来配合。

老黄绿了

但是我自己平时还是喜欢红色的。

平时灯光

我发现主板的装饰灯正好在公版 SUPER 卡上产生倒影,很有意思。

倒影

这个主板可以完美使用 OpenRGB,在 AUR 里安装之后把 /usr/lib/udev/rules.d/60-openrgb.rules 复制到 /etc/udev/rules.d 重启即可支持,不过对于 Addressable Header 上的设备也就是风扇或者灯条需要手动 resize zone 也就是填写灯珠个数,我用的利民 TL-C12S 系列经过尝试是 8 个灯珠。


更新(2020 年 12 月 1 日):我之前有考虑过换个机箱,倒不是先马鲁班不符合我的需求了,而是一些细节问题,比如我这个玻璃侧板不那么平,有一个角翘起来一点。然后我的前面板角落里虽然我十分注意还是被我磕了一下。以及当初从宿舍搬出来的时候没有把机箱包装拿出来,以后搬家可能不方便……我一开始本来打算再买一个同款然后把侧板和前面板换了得了,然后那天发现前面 USB 2.0 接口接声卡会破音……于是打算换一个别的机箱。

我个人其实比较喜欢白色的机箱,于是初步考察了一下,我很喜欢NZXT H1,但我又不可能装 ITX,然后 NZXT H710 看起来不错,我觉得很漂亮,而且我在 NGA 上看到过有人给它纯白色的前面板贴了个黑色的两仪式剪影贴纸很帅气,但是太大太贵了。为什么没选择 H510 呢,因为我有几个硬性需求,我日常使用总结出来的,是一般机箱评测和用户不会注意到的,比如最好不要显卡竖装(我个人肯定是不会竖装的),PCIE的螺丝锁孔也不要做成从机箱尾部凸出来的,其实这里的实际意思是机箱不要有太大的空洞,很多机箱的显卡竖装PCIE螺丝孔上面就那么开着口,凸出来的横着的PCIE螺丝孔虽然有一个覆盖物,但它总是不那么好用,于是这样排除掉了 NZXT H510, NZXT H510i 和 NZXT H710i。

然后我也不想要钢化玻璃打孔固定的,这个大部分人应该也都不想要。我个人而言还不想要附带一个只有某些软件才能控制的控制器的机箱(点名 NZXT CAM),虽然 NZXT 的水冷确实非常漂亮,但我不放心把电脑这些基础的硬件交给一个需要联网还经常不一定能不能连得上而且只有进了 Windows 才能启动的废物控制软件——我是 Linux 用户。同理我也讨厌雷蛇和罗技难用的鼠标驱动程序。明明我可以用 BIOS 控制风扇/水泵转速,用主板的 ARGB 插针控制风扇,为什么非要用那么难用的软件?所以肯定只考虑 H710 不考虑 H710i。

如果光是这些可能我就下单 H710 了,虽然贵一点,但是能满足需求也就不在乎了。可是我翻了翻各种帖子都提到了这个机箱的一个缺点:它的硬盘架需要用螺丝刀拧螺丝才能拆除。虽然现在两百块钱的机箱都能免工具调整硬盘仓了但我觉得理完线也没什么人总动机械硬盘所以这不是个问题,但是更严重的是许多人都说固定硬盘仓的螺丝非常难拧,甚至螺丝都花了也拧不下来。作为之前被笔记本内部拧花了的螺丝支配的恐惧的人,我可不要买个这样的样子货,于是只好放弃 NZXT。可能它的设计师平时是不用机械硬盘没有软件更新服务器连不上的问题也不在乎机箱开一大堆没有保护网的口的人吧!

然后我又开始研究买什么,看了一下联力的鬼斧似乎很不错,虽然造型上比起 NZXT 要难看很多,但是和其它的机箱比起来也算是鹤立鸡群了。而且相比于 NZXT 只是好看来说,它在设计上就考虑到了更多的功能性的细节。比如有单独的重启键(可怜的硬盘灯已经被时代抛弃了)和 LED 控制键而且 LED 控制器可以设置为转发主板的 ARGB 信号(看看人家!),还有磁吸的合页式的玻璃侧板方便随时打开(对我这种强迫症太友好了,拧螺丝搬侧板好累),而且它的电源仓设置了单独的合页门!你可以从前面打开,然后硬盘仓设计成了 NAS 那种可以从前面拿出来的样式(甚至官方还有热插拔配件,彻底变身 NAS)(看看人家!),对折腾型用户不要太友好。但我最后还是没选它,因为我发现这个机箱第一个 PCIE 挡板和它上面的边框之间的缝隙太太太太太大了——你们厂家在这里多加两毫米宽度会赔本吗?而且后来我又想了想,合页式多半不能完全贴合(要给合页留出空间,所以还是算了)。

然后我看到了海盗船新出的 4000D,纯白色的前面板 + 非外凸的 PCIE 螺丝孔 + 独立的重启键 + 滑动抽拉的硬盘仓 + 有卡口的侧板,虽然它只有一个前置 USB 3.0 和一个 Type-C,不过也不是不能接受,而且有独立的重启键(硬盘灯:四个人的接头我却不能拥有姓名)。虽然它也支持显卡竖装,但它显卡竖装的上面有一个封得比较严实的挡板,它的 PCIE 槽相对也没有太大缝隙(相对)。颜值虽然没有 NZXT H710 好看,但在简洁上也是吊打鬼斧了,于是决定入手这个。

到手了也发现这个箱子还不算完美,主要的问题在于以下几个:电源仓上边的挡板强度不够,虽然没什么人会拿这个地方承重,但是比我之前的先马鲁班软得多也太差劲了吧!好在机箱其它承重的组件都过得去。然后就是 PCIE 的螺丝孔和显卡的孔有错位!需要很大力气按着才能勉强拧上螺丝,我用的可是公版显卡,你不能说公版卡孔位不标准吧!最后最后,这个机箱底板并没有多延伸出一块盖住侧板的底部!虽然侧板不需要这个位置辅助固定(它有很多卡口固定住),但是明明你顶板是有延伸的,为什么底板要在这里留一条缝隙???总而言之就是搬机箱时候要注意手不要抠到侧板和机箱这里的小缝隙(挺窄的,手多往中间伸一点就好了),但是还是会让人担心抠这里把侧板抠变形了。不过总之看下去也没有更合适的选择了,所以就将就吧。送的两个风扇是 3 pin 的,不能 PWM 调速,真是抠门啊海盗船。

顺便还有个对我不是问题但对大部分人可能是问题的地方,这个机器的前面板里面有一大块金属防尘网,但是根据一些国外的评测,假如你在前面板装上风扇,可能会因为风扇吸气导致这个防尘网向里面贴而蹭到风扇扇叶。但是我装了三个反向进风的风扇在前边,风扇框架在防尘网一侧所以并不担心这个问题。我也推荐所有前面板不是透明玻璃的人在前面使用反向风扇,毕竟正常来说通过侧板看到机箱内部反向风扇的灯光才是最合适的(风扇框架不会出现在机箱内部)。并且说实话,前面板通风比美观更重要,真的没必要买玻璃前面板。

于是装好了以后正面和背面就是这样的,不要和我说机箱不是留了理线槽了吗为什么不把主板电源线理进去,海韵的主板电源线又粗又硬,连用这块机箱的盖线板盖住都很勉强了,扭来扭去塞到那个理线槽里根本不可能,我也不想让它接头的部分受太大的力。这块主板 Type-C 的接头在内存附近,虽然这是主流位置,但对于直插的 Type-C 线缆来说弯线同样也很困难,别的理线倒没遇到什么大问题。

正面

背面

白色机箱里面反光要亮一些,但是有灰尘也更明显了……

亮起来

前面侧板的灯光效果很好看,没买 Airflow 版一个是因为它挖孔太多太乱,另一个原因是以后我也打算贴上贴纸。

前面

今天看到 NGA 上有人是风扇白光配红色 ROG LOGO,我也试了一下蛮好看的,就是感觉屋子里又冷了 10 度……

白色

全景


接下来是喜闻乐见的 debug 时间,首先是 Linux 下的,相对比较好调:

三代 Ryzen 有一个 每次都返回 0xFFFFFFFF 作为随机数的 BUG,在我这主要影响 wireguard,巧的是购买前几天我刚读过这篇文章,AMD 已经发了新的固件修正错误,建议更新到主板厂家提供的最新 BIOS 版本一般即可解决,如果主板厂家最新的 BIOS 还没更新固件建议联系售后催一下。

在我这不知道为什么 GDM 有时候没法自动启动,但是手动切 tty start 又可以显示,查了一下 ArchWiki 的 GDM 页面 发现有解决方案,但是并没有原因,搜索了一下也没发现原因是什么。

我还发现有时候刚开机没多久很快就关机会卡在什么 systemd-udevd 进程没结束,最后 event loop failed + timed out,大概要卡好几分钟才关机,但如果你用一会再关机就没问题。检查好几次关机日志没发现问题,后来群友火眼金睛对比了完整日志发现有个叫 ucsi_ccg 的模块开机加载了两分钟,猜测是这个的问题,搜索一下发现是 5.3 内核里 NVIDIA 添加的相关代码,用来控制 NVIDIA 显卡上的 Type-C 接口的,可是我这块显卡根本就没提供 Type-C 接口!(以及很多笔记本内置的有输出的 N 卡也有这个问题。)临时在 /etc/modprobe.d/ 里加了个 blacklist ucsi_ccg 的 conf 屏蔽了这个,好像没什么不良影响……

然后是奇怪的 Windows 的问题,我一开始装的 LTSC,不知道怎么回事输入法没了……折腾无果只能重装。以及现在除了不要联网装 Windows 之外(否则会强制你登录微软账户然后用你名字拼音前五位做用户名),还得不要联网装 NVIDIA 驱动,否则 Win10 自动更新驱动会给你安装 DCH 版的,虽然没什么影响(只是在 NVIDIA 官网升级驱动时候不能选标准选 DCH),但是就是让人很不爽。声卡驱动要装主板厂商的,Win10 自带的只能输出,不能接麦克风录音,然后如果麦克风声音很小就打开 Realtek 的声音控制程序,在右上角齿轮里取消掉什么把所有输入结合到一起的设定(什么乱七八糟的玩意!)。还有要关掉快速启动,不然直接开机会卡在黑屏一个鼠标光标……反正就很烦人。

更新:还有一个奇怪的问题是 Win10 关机重启也要卡很久,上网搜了一下全是一些忽悠小白的办法,经过我不懈搜索发现了一个熟悉的名字 UCSI!微软承认存在 UCSI 问题,既然又是这个 UCSI,多半还是 NVIDIA Type-C 的问题!反正我没这个接口,果断重新安装 N 卡驱动,选择清洁安装(删除旧驱动)并不勾选 Type-C Driver,问题解决……

最后既然设备到位了,大概就每周一三四五晚上八点半在 Bilibili 4312991 直播间 播一个半小时游戏,反正我玩什么播什么,大概就 CSGO Dota2 PUBG 什么的吧,尼尔也有可能,反正不是恶心反胃的就可以。周末随缘直播,如果没什么事情白天就播一会,周二可能晚一点开播因为有课。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at January 15, 2021 11:46 AM

January 14, 2021

中文社区新闻

手册页索引服务

我们非常高兴地宣布我们最新的公共服务: 在 man.archlinux.org 公开的手册页索引站,用来发布我们所有软件包中所包含的手册页(man pages)并提供 搜索在线浏览功能。比如可以试一试 tar 的手册页

也可以在每个软件包的详情页面的侧边栏中看到新增的手册页链接。感谢我们的 Wiki 管理员 lahwaacz 开发了 archmanweb 实现这一功能。

尽管网上已经很多其它的手册页索引网站,通过发布与我们提供的软件包相符版本的手册页,我们希望能进一步改进 Arch 的可用性和文档。

by farseerfc at January 14, 2021 06:52 AM

January 07, 2021

Leo Shen

Homelab Project: 6 months in

At the summer of 2020, I started the Homelab project. Now, 6 months later, it's time to evaluate how the thing goes. Hardware configuration Previously, I've been using an AMD Ryzen 2200g with a garden-variety A320 motherboard. However, there had been two incidents, of different reasons, which prevented the OS from booting, and I had to remotely instruct my parents to fix it using TTY. Needless to say, these didn't go well.

January 07, 2021 07:01 AM

January 04, 2021

Alynx Zhou

StackHarbor 的 2020 尾记

我最近思考了一下,总是记不起来去年的总结写了什么,结果翻了一下博客发现我的记忆力是对的——我去年还真的就忘了写总结。

今年的总结因为各种原因写的稍微晚了一点,不过总之还算是写了,比忘记写要好得多吧!

以前小时候总是觉得一年过得很慢,要过很久才到新一年放烟花吃饺子,但是现在觉得一年过得很快,可能要忙的事情多了就会觉得时间不够用。但我一般来说又不觉得自己做了什么值得记录的事情,看到别人的博客年终总结写的特别充实,又是自己出国求学又是自己找实习转正的,但是到了我自己总觉得这些也没什么好写的。再加上我是个相当讨厌计划的人,所以也没什么“检查自己一年的完成度”的机会。

不过我在那些不错的年终总结里面还是学到了一些东西,所以打算也写点类似的。不过我虽然是程序员,写文章还是习惯从头到尾写,不擅长做那些分类加标题的事情,所以就想到哪写到哪。

2020 年感觉最不错的事情大概是加入某绿色蜥蜴工作,虽然这个严格来说从 2019 年就开始了,但是我去年忘记写总结了……多亏了同学的推荐得到了一份实习,面试感觉很好,没有考什么我特别不擅长的算法题而是一些实践性的知识,这个我还是挺擅长的。然后同事也都相当好相处,一开始是测试相关的工作,也了解了很多测试方面的知识,甚至还写了点 perl(虽然只了解皮毛),总之是很有意思的经历,然后更意外的是领导居然主动问我有没有什么别的感兴趣的领域,因为我一直是 GNOME 用户所以对 GNOME 维护挺感兴趣的,结果后面就转到 GNOME 组去实习了(这也太好了吧天哪)。然后就是快毕业了需要准备正式工作,一般来说这边没有类似国内互联网企业那样招一大堆实习生然后给几个转正名额竞(yang)争(gu)的途径,并且他们也几乎不进行校招,对毕业生和社招一视同仁,虽然我个人很想在这工作,但是如果想留下来的话还挺看运气的。这时候领导又和我说正好组里有空缺职位,可以安排面试,只要能保证一直实习到正式入职就可以了,于是又十分幸运的毕业之后正式入职。总之能在自己感兴趣的领域做工作已经是十分幸运了,然后待遇相对来说也不错,特别是看多了加班猝死的新闻,心里更加满足了……同事也都很友善,而且都是技术类型的,做的又都是开源相关,平时也很聊得来。今年整体来说不是那么容易找工作的,我都要反思我为什么那么幸运了……顺便由于疫情原因,我司今年一直是在家办公状态,也节省了好多通勤的时间金钱……

在找工作这方面我实在是没什么经验可谈,我太靠运气了……如果非要说的话,就是平时自己多学习多写写程序吧……

因为工作的原因今年一直是自己在外面住的,房租好贵啊……至于自己住虽然挺安静的但也挺麻烦的……把东西从学校搬出来也花了不少麻烦。

今年个人项目方面没做什么新东西,去年把 Hikaru 从 CoffeeScript 换成 JavaScript 之后基本就只是写文档、加测试、改功能,大改是去掉了 cheerio 改成自己实现了一部分功能,谁叫他们一年不……主题方面给 ARIA 做了个大改是去掉了 jQuery 加上了暗色模式,当然我自己看起来界面并没有什么变化。大一时候写的 FlipClock 努努力改成了 CMake,然后这样就可以跨平台做成 Windows 屏保了。顺便了解了一下怎么在 Android 上面运行 SDL,做了个 Android 的 FlipClock。而至于我的弹钢琴页面和 Telegram Bot,我已经忘记是今年还是去年写的了……

今年折腾了一遍我的电脑,因为终于有时间和钱玩自己的台式机了,仔细想想好像把之前的能换的都换了,27 寸的显示器对没有双屏空间的人来说提升了不少工作效率,5800X 打游戏也很爽,就是钱包不太舒服……

口琴方面今年年末又高产起来了,而且开始剪视频,发现达芬奇可以在 Linux 下面用(虽然有些限制)(Adobe 看看人家!),而且还挺流畅的,于是看了影视飓风的达芬奇教程学了一些基本的剪辑知识,为了用的更舒服还闲鱼买了个加密狗(假货很多,安全下车),今后可能剪视频的频率会逐渐增多,就当练习新技能了。

手机打算再用一年,今年手机厂商出的都是什么垃圾?我现在也想清楚了反正手机又不能给我带来收入,有这个钱还不如投资到台式机上,希望各种换手机患者也考虑一下,我现在是能用就行了。除非哪个厂家出一个摄像头不丑还有耳机孔最好还是直屏系统不要乱删乱改的旗舰机。

动漫除了看电磁炮 T 以外就是看了紫罗兰永恒花园,一开始很多人吹导致我对这个比较反感,实际看了以后觉得还是很不错的,所以吹得太过果然会招黑吗……电磁炮 T 总之中规中矩,能有第三部已经很不错了,我还想看第四部……、

认识了一些新朋友,同时很多老朋友也都有各自要忙的事情,总之几乎没什么人一起打游戏了……不过经常能和蓝猫她们一起出去玩还是避免了成为死宅的命运,本来我都打算在家打游戏跨年了,最后和蓝猫狐狸一起吃了海底捞,虽然三点才回家导致第二天犯了鼻炎,不过还是非常开心。

年末通关了 Titanfall 2,剧情很短,中规中矩,但是就已经是非常不错了,除了操作不适合我这个手残以外都很适合我。今年几乎没怎么玩 CSGO,但是下班之后有很多空余时间基本都投入在 Dota 2 上面了,虽然我也看很久 Dota 2 了,但是玩起来确实很难……不过我这一年一直都沉迷在中单光一直播里面,已经成了我玩 Dota 2 的动力了……现在多少也算入门了,虽然偶尔还是操作不过来,但至少明白是个怎么回事了。中单光一的直播真的很好看!正人君子,皮又好看,说话又好听,打游戏厉害,又很温柔。一开始我只是看他打 Dota,反正讲围棋我又看不懂,但我发现他读围棋棋手传记有意思多了,已经进入追小说模式了……拖到现在才写年终总结也是因为坐了 16 个小时的火车跑到上海去看 VirtuaReal 的第一次线下 Live,不过互动环节没抽到我实在是令人沮丧,我太非了,那么多人根本没我的机会呜呜呜呜呜……

就写这些吧,希望 2021 年大家的生活都能变得顺利!

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at January 04, 2021 09:21 AM

中文社区新闻

Arch Linux 邮件列表 id 变更

由于我们的垃圾邮件应对机制,我们不得不迁移邮件列表,原本从 @archlinux.org 发出的邮件变更为从 @lists.archlinux.org 域名发出。

发送邮件到邮件列表不受影响,发往 @archlinux.org 还能继续使用,邮件会被自动转发。

需要用户操作的唯一变化在于匹配 From 和 List-id 字段的过滤器和规则需要相应更改。

by farseerfc at January 04, 2021 12:16 AM

December 29, 2020

Leo Shen

Fix incompatible bytes library for actix-web and tokio

When attemping to build some web app with actix-web, I ran into this issue: 1 2 3 4 5 6 7 8 error[E0271]: type mismatch resolving `<fn(bytes::BytesMut) -> bytes::Bytes {bytes::BytesMut::freeze} as FnOnce<(bytes::BytesMut,)>>::Output == actix_web::web::Bytes` --> src/api/get.rs:35:47 | 35 | return HttpResponse::Ok().streaming(s); | ^^^^^^^^^ expected struct `bytes::Bytes`, found struct `actix_web::web::Bytes` | = note: perhaps two different versions of crate `bytes` are being used? = note: required because of the requirements on the impl of `futures_util::fns::FnOnce1<bytes::BytesMut>` for `fn(bytes::BytesMut) -> bytes::Bytes {bytes::BytesMut::freeze}` This happens when I'm trying to utilize FramedRead in Tokio in order to stream the content while sending it in acitx-web (instead of reading them all into memory before sending it out).

December 29, 2020 12:05 PM

December 12, 2020

百合仙子

一次失败的 KDE 尝试

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

前些天尝试了一下 KDE 桌面环境,不过实在是没能用下去。

首先要说的是,KDE 桌面确实漂亮,非常养眼。设置项也挺多,可定制性还是挺不错的。只可惜问题同样很多。

首先是显示器缩放的问题。受限于 X11,KDE 只能设置一下全局的缩放比例。所以我们只好缩放显示器显示的画面。不幸的是,一向相当体贴的 KDE 此时却笨笨的,在使用 xrandr 设置好之后需要重启 plasmashell 来使其获取 xrandr 的设置更新

kquitapp5 plasmashell && kstart5 plasmashell

KDE 的设置项很多,分门别类地在「设置」应用程序中集中列出来,然而问题也由此产生:同时只会显示一个「设置」窗口。也就是说,我配置快捷键时,想去窗口管理器那边看一看,调整一点选项,就必须放弃我当前打开的快捷键视图,放弃我键入的搜索词,并且选择「应用」或者「放弃」更改,才能切换到另一个「设置」组件中去。即使从 krunner 里打开某个组件的设置,它也会找到并更新已有的窗口。

我知道 Windows 10 也是这么个「单任务」设置的风格。可 Windows 10 也没有这么多可以设置的地方呀。后来获知有个命令可以打开单独组件的设置窗口。很不方便。它被隐藏起来的原因是这种窗口不能返回到组件列表界面,会让用户困惑。可是,为什么我不能同时打开「设置」的不同组件的多个窗口?单独组件的窗口会让用户困惑,那就不要用单独组件的窗口就好了嘛。

KDE 桌面还有个问题:启动特别慢。登录进入界面要好久,启动一个程序,它的图标也要跳好久窗口才会出现。不知道它在干什么。我甚至怀疑它是为了展示启动动画而故意推迟界面的显示。

KDE 有提供丰富的桌面部件。我往副显示器上放了一些系统状态的监视器——CPU、磁盘、网络啥的。然后问题来了:我凑齐了四个部件刚好形成2x2的网格,可是我要怎么对齐它们呢?并没有对齐的选项,也没有吸附的功能。在我找到它使用的配置文件并手动修改之前,我只能用肉眼瞅。可计算机不就是用来做这种人不擅长而机器擅长的事情的吗?

终端我还是用 GNOME Terminal,因为有些特性(比如超链接)只有它支持。但又出现问题了:它启动之后,pin 它的任务图标,或者通过任务栏图标创建新实例均会失败。把它 pin 到任务栏上,需要从主菜单的右键菜单里操作。即使这样,启动之后终端窗口还是会位于新的图标,旧图标还是不对应任何窗口。后来查了一下,GNOME 的东西都没有主动支持启动通知,导致 KDE 很多时候只能猜测,而这次它猜错了。解决的办法是给 GNOME Terminal 的 .desktop 文件加上正确的 StartupWMClass 项。这其实不是 KDE 的问题,但也没办法。KDE 不想为别人擦屁股,GNOME 不在意自己的软件在别的桌面上的可用性。

不过 Qt 写的 flameshot 我就不知道是怎么回事了。具体情况不记得了,反正就是显示异常。好像是全黑吧。我没来得及 debug 这个。

最后,让我决定放弃 KDE 的点来了:我设置不了我需要的窗口管理快捷键

切换窗口,默认是 Alt-tab 的那个,我好不容易在「快捷键」设置里找到了添加更多快捷键的方式,但我发现除了 Alt-tab,我自定义的都不能连续切换窗口。按一下,切换一下,然后就切不动了,只能放开快捷键。后来了解到这是设置更新方面的问题,kwin_x11 --replace一下就有效了。

切窗口其实问题不大。问题大的是切显示器屏幕。两个功能:一、把焦点切到另一个屏幕;二、把当前窗口移到另一个屏幕上。

前者可以勾选「分隔屏幕焦点」选项,然后调整一下「阻止盗取焦点」的级别。我也不知道这个级别都是啥意思。「无」我能理解,「低」「中」「高」「终极」都是些啥?反正调整一下,确实可以把窗口焦点切换到另一个屏幕去了,除了鼠标不会跟着过去!另外测试过程中,有时候焦点会丢失——我不知道当前什么窗口获得了焦点,也不知道接下来谁会获得焦点。比 Mac OS X 里焦点跑到一个窗口也没有的 Finder 上还要神秘。

不过这个倒是可以自己写个脚本解决:使用 xrandr 获取屏幕的大小和位置,通过 X 的接口获取鼠标的位置并通过 Xtest 扩展来移动它,然后再用某个 X 的接口去设置窗口焦点——完全绕过 KDE 的功能。

然后我被另一个问题难住了——我怎么把窗口移到另一个屏幕上并且把焦点也移过去呢?使用文档匮乏的 kwin script 是可以把窗口移过去,然后我没能找到移动鼠标光标的 API。通过 X 是可以移窗口的同时移鼠标,但是我拿不到带窗口装饰的窗口位置信息。kwin 有一个 getWindowInfo 的 D-Bus 接口,但是它接收的那个 UUID 参数,我没找到获取的方法。

总结一下,KDE 对快捷键的支持并没有想像的那么好,尤其是多显示器的支持。快捷键的设置是通过图形界面来操作的,虽然直观但是对于大量快捷键的管理来说非常困难。而对于大显示器来说,通过快捷键来管理窗口是十分必要的——因为我更难肉眼找到我的鼠标光标去了哪里。

接下来,我打算一边忍受着 Awesome 3.5.9 的旧与 bug,一边尝试将 i3 改造成我需要的样子。

by 依云 at December 12, 2020 02:55 PM

December 06, 2020

百合仙子

i3 的 scratchpad 处理逻辑

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

i3 有个东西叫「scratchpad」,和我在 Awesome 里用的 run_or_raise 功能有些类似。

我的需求是某些浮动窗口可以「招之即来,挥之即去」。上次尝试切换 i3 遇到的一大麻烦就是,我经常从终端里启动图形界面的程序,而启动完之后我得手动给我的终端找个地方放着。i3 不支持最小化,也只有十个带数字快捷键、可以快速访问的工作区,所以 scratchpad 很重要,但是它的行为我有些捉摸不定。

首先是 move scratchpad 这个命令。它会把当前 con(窗口或者容器)浮动、取消全屏,然后移到一个叫 __i3_scratch 的不显示的工作区。

然后是 scratchpad show 命令(动词放后边了)。如果没有指定条件,它有如下复杂的处理逻辑:

  • 检查当前窗口是不是去过 scratchpad。如果是,就把它丢回去。
  • 否则检查当前工作区是否有另外的 scratchpad 窗口。如果有,就给它焦点。
  • 否则检查其它工作区是否有另外的 scratchpad 窗口。如果有,就把它移过来。
  • 否则把 __i3_scratch 里最久没有「见到光」的窗口移过来。

如果指定了条件,那么这样检查匹配的窗口:

  • 如果窗口不曾去过 scratchpad,什么也不做。
  • 否则如果窗口去过 scratchpad 并且在当前工作区,就隐藏它。
  • 否则就把它移过去。

总结一下,就是「回去,或者回来」。虽然动作的名字叫「show」,但其实是一个类似于 toggle 的功能。它的麻烦之处在于:如果你有多个去过 scratchpad 的窗口,你很难控制出现的是哪个窗口。一个绕过这个问题的办法是,总是带条件地使用 scratchpad。另一个小麻烦是:没有办法在匹配的窗口已经显示的时候,不要把它隐藏掉——有时候我只是习惯性地呼叫我的终端,而不看它是不是已经在我面前了。

对于浮动窗口,i3 有很多奇怪的限制,或者说是未实现:

  • 不支持最小化
  • 浮动窗口也不能显示在平铺窗口之下(加上上一条,就是没办法暂时藏起来)
  • 不支持最大化(手动调整窗口大小无法自动适配显示器大小,也没有「恢复」一说)
  • 不支持显示在最上层(当你在 GIMP 里开了一堆图片需要局部对比时)
  • 有全屏窗口时不能显示浮动窗口(看视频无法临时使用浮动窗口查个单词啥的)
  • 切换窗口时,平铺窗口和浮动窗口是隔绝的(需要单独的快捷键来切换)

by 依云 at December 06, 2020 06:05 AM

November 26, 2020

Alynx Zhou

解决 Spleeter 愚蠢的依赖问题

我倒不是对机器学习有什么莫名的偏见。事实上有些只能用机器学习搞定的东西我也很支持用机器学习解决,比如 waifu2x 这种增加图片分辨率的或者 Spleeter 这种分离人声和伴奏的,用传统的分离人声的方法就是不能完美解决这类问题,而机器学习模糊分类则可以无限接近完美解决。我讨厌的有两个,一个是传统方法很好解决的东西非要用机器学习解决,另一个是混乱的机器学习项目,后者更严重。

不知道是因为什么原因,许多机器学习从业者似乎都缺乏整理代码和依赖的能力——能轻松地把一个这类开源项目打包简直是奇迹,更多时候别提打包了,你想自己安装然后跑起来都不太现实,哪怕是同样做机器学习的其他人也有这样的苦恼:实现者似乎把论文写好然后在自己电脑上能跑起来就心满意足了,丢出来一份没什么文档的代码,不折腾几天根本不知道这坨代码需要装些什么才能跑起来。对我这种洁癖用户就更严重了,我可不希望 pip 在我的系统里塞一堆乱七八糟的东西,这个需要其他项目才能避免全局安装的程序比起 npm 真是差远了,解决依赖的能力也远不如 pacman 和 npm。

然后有人发明了 conda 和 docker 这样的 辣鸡 项目来拯救其他的辣鸡,但事实上不过是变得更糟而已——它们给了一些懒人名正言顺不维护项目的借口——反正我丢一个 conda 配置或者 docker 文件上去,你们拉上一大堆和我一样的过期依赖就能跑了,至于占了你多少硬盘,干不干净,关我什么事?如果不是物理限制,恐怕这些懒人会把整个宇宙都塞进去。而且他们再也不会管版本更新,什么 tensorflow 2 与我何干?用了我的 docker 你的系统里就会有几百个不同的过期的 tensorflow 1,买硬盘就完了呗?这和买显卡就完了呗还真是一路货色,这样依赖商业公司的产品,总有一天感觉会被割韭菜。

今天我用到的这个 Spleeter 某种意义上也是这样的一坨辣鸡,它的仓库里面的 README 是完全过时的,但这起码是件好事:代码还在更新。只是你按照文档是没办法顺利的搞定它的,我总结了一下我的解决方案,可能不适合其他人,但我大概知道怎么解决了。

这个文档有多离谱呢?它说你如果使用 GPU 加速的版本,必须从 conda-forge 安装,pip 和 GitHub 的版本都只有 CPU——那你这也是按揭开源?然后整个文档没有一个地方告诉我我需要单独下载训练好的模型才能跑起来,直接运行程序则丢一个段错误——可能他们搞机器学习的人觉得下模型是常识不需要说吧。

如果直接按它的方法运行 conda install -c conda-forge spleeter-gpu,你会得到一个不能用 GPU 的 GPU 版,这实在是太搞笑了。我捏着鼻子用 conda 就是为了能让你一步把这一坨东西给我弄好,现在你说你弄不好?

但是有一点好处就是实际上他们的代码已经更新到支持 tensorflow 2 了,所以其实完全不需要用那个过期的 conda-forge 的版本,你完全可以直接在 Python 3.8 里面 pip install spleeter-gpu 安装最新的版本——然后我又遇到了依赖问题,我的一些系统软件包依赖 numpy,然后 Arch 官方源里的 numpy 版本还是比 spleeter-gpu 依赖的 tensorflow 依赖的 numpy 版本新——你们写 Python 的人真麻烦,版本号兼容性是可以随便 break 的吗?

所以这时候我还是用 conda 解决,总之就是搞一个和系统独立的虚拟环境(npm:这不应该是内置功能吗?还需要用商业软件?)。总之去 TUNA 搞一个 Miniconda 3 最新的安装包来,然后直接安装,看着它往你的 shell 配置里塞一坨辣鸡(我把实际用到的命令拿出来做了个 alias,这样就可以只在我需要的时候打开 conda 了)。

为什么使用 miniconda 而不是 anaconda?因为 anaconda 带的那个图形界面根本用不了,miniconda 够用了。

然后创建一个 Python 3.8 的环境,高了低了都不行,真是难伺候,同时别忘了带上 cuda 和 cudnn,不然它一声不吭的就会只能用 CPU:

$ conda create -n spleeter python=3.8 cudatoolkit cudnn

cuda 不叫 cuda,叫 cudatoolkit 就离谱。

然后切进去:

$ conda activate spleeter

然后装 spleeter-gpu 到 conda 新创建的这个 Python 环境:

$ pip install spleeter-gpu

所有的依赖应该 pip 都会解决,但这个弱智有时候还会说我本机已经装了 numpy 1.19.4,比 tensorflow 需要的版本高,但我都创建虚拟环境了你还读取我系统的干嘛?不过其实好像也不影响使用,或者此时可以 pip install numpy==1.18.5

然后到 https://github.com/deezer/spleeter/releases 去下载训练好的模型,这也是个弱智的地方:哪有 tar 打包不把目录本身打进去的?然后还得给这个程序创建一个工作目录,因为它是写死的到当前目录下面的 pretrained_models 下面去找模型。

$ mkdir -p spleeter/pretrained_models
$ cd spleeter/pretrained_models

我这里用 2stems 的模型示范,因为我只需要分离人声和伴奏:

$ wget -c 'https://github.com/deezer/spleeter/releases/download/v1.4.0/2stems.tar.gz'
$ mkdir 2stems
$ cd 2stems
$ tar -xpvzf ../2stems.tar.gz

因为他们打 tar 包时候没把目录打进去,所以别忘了自己创建目录!

然后回到你的工作目录就可以用了,我这里结构是 spleeter/pretrained_models/2stems 所以工作目录就是 spleeter

$ cd ../../
$ spleeter separate -i 你要处理的歌曲 -p spleeter:2stems -o 输出目录 -B tensorflow

如果你想使用其他的几个模型,那就把 2stems 改成其他模型的名字,但是这里还有个开发者脑子抽了的地方:带有 -finetune 的是高品质模型,它们的模型目录名字应该是比如 2stems-finetune,但是参数名字却不是这个而是 -p spleeter:2stems-16kHz,文档里当然是没找到的,我觉得应该揍开发者一顿让他老实写文档(不过我又看了一下,这个 finetune 对于分离音轨没什么用好像)。

我这里必须使用 -B tensorflow 才会走显卡加速。

用完了就可以 conda deactivate 退出虚拟环境,要用的时候别忘了 conda activate spleeter 切换进来,

遇到类似问题的同学可以参考我的文章,但是因为这个处理过程影响因素太多了,如果你的不能用我也没什么办法。

回头一看,这个项目犯了一大堆禁忌:难以解决的依赖,写死的模型路径还有匮乏的文档,导致配环境就要配一大堆。当然,好在他们还是在努力更新跟上依赖而不是撒手不管让它慢慢死去,并且功能非常好。真正对开发者友好应该是不需要配环境的,比如在 Linux 下面开发软件,包管理已经帮你考虑好各种依赖了。只有像 Windows 或者 Android 这种不以开发者为中心或者是许多机器学习项目这种“数据好看就行”的地方才会有这么多麻烦。真的很希望这些人能补一点务实的基础,不要让他们的软件这么难用。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at November 26, 2020 08:43 AM

November 23, 2020

frantic1048

Espresto - 忍野忍 Clear materials

Shinobu

到了一两周的小忍的照片,总算是搞出来了。当时看到介绍的照片还不错,而且还是景品,就果断入手了,到手一看,非常良心!

也是这个月,升级了一下之前的小相机 Canon G7X Mark II (G7X2)到新出的 Sony Alpha 7C(A7C)。新机器到手后兴致勃勃地搭起环境拍了一波,传到到电脑之后——Darktable (脑补的)一副阿库娅脸提示读到的是不认识的 RAW 格式,拜拜。

作为多年 Arch 用户对设备过新造成的问题早有体会,比如 18 年底装机,入手首发 RX590,旧系统直接黑屏,最新的 live iso 上来也是黑屏,折腾半天 GPU 的驱动问题解决之后接着主板也来一脚,让我新装机器的激情全撒在想方设法让系统启动上了,到最后终于成功启动,已经内心毫无波动 ヾ(°ω。ヽ=ノ °ω。)ノ

简单去 Darktable 的 issue 列表翻了一下,很巧数天前就有人报告关于 A7C 支持的问题,对照了一下已有的别的相机支持的 issue 的创建时间之后,这样等下去怕是半年都不定会有支持,我不能接受!于是去 Darktable 历史翻了一通的新相机支持的 PR,发现并不是那么深奥的操作,糊了俩看起来能用的 PR,今天(2020-11-23)总算是过了上游 Review 合进去了,终于确定之前自己 patch 的版本是 ok 的了!

接下来是小忍的时间。

右边的头发尖有一点点歪,不知道是不是故意的,还是只是运输的时候给搞弯了,尝试一番也没能扭直就这样吧,不要盯着看也不会察觉。和四糸乃作为同样暗色服装的手办来说,服装上的(只有拍照时候比较容易凸显出来的)细微瑕疵相对少很多,修图工作量大大减小,拯救了我的眼睛。

这个 JPEG 输出 banding 都压出来了有点难受(没看到?那就是没问题!),等工具链的 AVIF 站起来了再改善一下画质。

Shinobu Shinobu Shinobu Shinobu

Shinobu Shinobu Shinobu Shinobu

半透明的材质效果很棒,群摆上的细节也比较稳定。

Shinobu

我好了!

Shinobu

November 23, 2020 12:00 AM

November 21, 2020

百合仙子

HiDPI 配置记录

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

首先,我是用 X11 窗口系统的,不同屏幕分别设置肯定没戏。所以只好让笔记本电脑的屏幕迁就一下我的4K主屏啦,把笔记本屏幕缩放一下。算一下 scale 值:192 / 120 = 1.6。不是整数,会糊,可总比放大两倍的巨大界面要好。

xrandr --output eDP-1 --scale 1.6 --auto --output DP-2 --auto --pos 3072x0 --primary --fb 6912x2160

这里要注意的是,要指定--pos(或者--panning),不然会重叠;要指定--fb,不然鼠标可能会有部分区域去不了。

然后开始设置。本来我是尝试了一下 KDE 的,但因为我将在下一篇文章中写的原因而放弃,回到了 Awesome。不过也不是全无收获。我把 KDE 的配置方案拿过来用了。你想问怎么拿的?我 btrfs 的文件系统,做好快照再 rsync -n 对比一下它动了哪些文件就有了。

首先是 X11 的资源。在~/.Xresources里写上Xft.dpi: 192,然后xrdb -merge ~/.Xresources一下就好了。顺便再xrandr --dpi 192一下,听说有些程序会读这个。

然后是 GTK。GTK 2 就放弃吧,没办法。文字会按设置的 Xft.dpi 放大,图标啥的不会。GTk 3,要设置两个环境变量:

export GDK_SCALE=2 GDK_DPI_SCALE=0.5

前一个是把界面放大,后一个是把文字缩回去,因为文字已经按 Xft.dpi 放大过,不能再放大一次了。

再然后是 Qt。Qt4 早卸载干净了不用管。Qt5 嘛,也不用管。它自己会处理好。有个按不同屏幕缩放的环境变量QT_SCREEN_SCALE_FACTORS,效果跟 Windows 10 差不多的。但是我为了照顾其它程序已经把屏幕给 scale 过了,就不需要设置这个了。你要设置个QT_AUTO_SCREEN_SCALE_FACTOR=0也行,但这个是默认行为。

最后是个别的程序。

Telegram 直接在设置里关掉「默认界面缩放比例」并且设置缩放比例为 300% 就好了。我也不知道为什么,Telegram 默认的字总是很小。之前 120dpi 的时候我要 200% 缩放,现在 192dpi 需要 300% 缩放了。

YouTube,就是那个网站啦。它其实没什么显示上的问题,只是死活不会给我自动选择 1080p 以上的分辨率。经过仔细二分测试之后发现,把火狐的配置文件夹下的storage/default/https+++www.youtube.com目录删掉之后就好了。没发现删掉这个会有其它影响。

mpv 要修改配置文件,加上no-hidpi-window-scale参数,不然会把视频自动放大,4K视频一打开会只能看到四分之一的画面。加上这个参数,默认窗口大小时,一个视频里的像素会对应一个显示器上的像素,不大不小刚刚好。mpv 文档上说这是 OS X 系统上的默认行为,可我这是 Linux 桌面啊,你把别的平台上的习惯搬过来是几个意思?另外我加了个demuxer-readahead-secs = 20选项。我的大文件都在机械硬盘上,4K 码率又比较高,不多预读一点容易卡。

我的 qemu 之前使用的是-display gtk,也坏掉了。窗口那么大,虚拟机只用左下角那里四分之一的空间。spicy 也有问题,会告诉虚拟机只有 1080p。解决方法是 unset GDK_SCALE GDK_DPI_SCALE。它们在放大了自己的界面的同时,把显示的虚拟机的内容也给放大了,所以干脆叫它们别动。也没什么别的影响。

哦还有 Zoom。设置个QT_AUTO_SCREEN_SCALE_FACTOR=1似乎就好了?我试了一下QT_SCREEN_SCALE_FACTORS,会导致很怪异的行为。

以上解决了显示大小的问题,但我发现还有个问题:我的鼠标光标时大时小的……从 KDE 那边弄来几个设置之后就好了,而且主题也更加一致了呢。

首先是设置 xcursor 环境变量:

export XCURSOR_THEME=Vanilla-DMZ XCURSOR_SIZE=36

听说对应的 X 资源大家都不理睬,那我也就不设好了。

然后是 GTK 2 的~/.gtkrc-2.0文件里写上:

gtk-cursor-theme-name = "Vanilla-DMZ"
gtk-cursor-theme-size = 36

再接下来是 GTK 3 的~/.config/gtk-3.0/settings.ini

[Settings]
gtk-cursor-theme-name = Vanilla-DMZ
gtk-cursor-theme-size = 36

然后又没了。天知道为什么 Qt 那边啥都不干就好好的,GTK 却这么麻烦。

啊,你问这些环境变量在哪里设?我给写~/.xprofile里了。不过这还不够。有些 GUI 程序会由用户的 systemd启动(比如我的 Telegram 是由 systemd 启动的,为了在内存用得太多的时候自动重启),有些 GUI 程序会由 D-Bus 激活(比如 gnome-terminal)。这些是和登录会话分开的,所以要手动导入一下。以下是我的 .xprofile 中导入图形界面相关环境变量的部分:

_envs=(
  GDK_SCALE GDK_DPI_SCALE
  XCURSOR_THEME XCURSOR_SIZE
  XMODIFIERS QT_IM_MODULE GTK_IM_MODULE
  LIBVA_DRIVER_NAME GST_VAAPI_ALL_DRIVERS
)
dbus-update-activation-environment "${_envs[@]}"
systemctl --user import-environment "${_envs[@]}"

至于登录界面怎么办,我是在 lightdm 的 display-setup-script 里,跑了跑 xrandr,设置了一下 Xft.dpi 资源。环境变量啥的没动,反正用不上。当然你也可以去改~lightdm/.pam_environment来设环境变量,反正现在 Arch Linux 还是读它的。别的 dm 同理。

by 依云 at November 21, 2020 10:12 AM

November 20, 2020

百合仙子

让 QEMU 使用 SPICE 协议

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

缘起

我就是买了个4K显示器,咋这么多事呢……(有两篇文章还在路上)

第一个问题是,我的显卡 vGPU 最高只支持 1920x1200 的分辨率。行吧,我缩放成了吧?嘿嘿,-display gtk 的缩放不会保持比例,我只好算了算最大保持比例的大小,然后窗口切成浮动,再调用命令调整到指定的大小:

sleep 1 && xdotool getactivewindow windowsize 3280 2122

然后还要居中放置一下。多麻烦!

第二个问题是,我想在虚拟机里试试 i3,但是我的按键总是会被外边捕获。Super 键基本上是 Awesome 在用,而 Alt 键会撞上这个 GTK 窗口菜单栏的快捷键。

配置

经过多番尝试和摸索之后,确定了如下的参数:

  -display egl-headless,gl=on,rendernode=/dev/dri/renderD128
  -spice unix,addr=/run/user/1000/qemu/ArchKDE/spice.sock,disable-ticketing
  -device virtio-serial-pci
  -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0
  -chardev spicevmc,id=spicechannel0,name=vdagent

当然要gl=on啦,不然我怎么玩特效,我还不如回到 vbox 去呢(啊不,我的 vbox 不喜欢 btrfs,经常出错然后只读挂载,所以已经被删掉了)。要指定rendernode,不然它会 fallback 到 llvmpipe,然后还段错误崩掉……这也是我不-display spice-app的原因。

后边三行是从这个 QEMU + Spice with Copy & Paste 抄来的。搜索问题时不小心遇见,然后解决了这个我一直没有处理的问题。SPICE 不但能共享剪贴板,而且还支持 PRIMARY 选择区呢~虚拟机里要安装 spice-vdagent 并启动相应的服务。

然后是客户端的选择。一个很神奇的地方是,virt-viewer 看上去轻量,实际上只是选项少而已。它不光拖进来个 gtk-vnc 依赖,还把 libvirt 都给我带上了……然后 GNOME 的 vinagre,我不知道为啥,它就是连不上我的 spice+unix 地址。哦对了,virt-viewer 直接敲命令调用也是连不上,只能用 xdg-open 才能正常打开。

virt-viewer 有个依赖叫 spice-gtk。我试着 pacman -Ql 了一下,还真找到个 spicy 工具。比 virt-viewer 轻量多了,选项也更为丰富,比如可以选择不 grab 键盘。

一点额外的东西

在群里听说了 virtio-fs 共享方案,听说比 virtio + 9p 更高效。然后我用它成功取代了之前用的 NFS 方案(反正我的 vbox 虚拟机已经被删掉啦)。NFS 的服务也可以卸载啦(开放一堆端口到公网,看着有点怕怕的,虽然是 IPv6 地址不太会被扫到,但知道我的地址的咋办呢)。

virtio-fs 相比 virtio + 9p 的另一个优点是,和 NFS 一样,virtiofsd 是以 root 权限运行的,所以可以写入我的 pacman 缓存。qemu 那个 9p 似乎没有办法。至于启动嘛,用 systemd abstract socket 触发一下就好了。

另外,我使用 GVT-g 和 virtio 输出视频信号时,均遇到了声音在视频画面变化时声音卡顿的情况。一个绕过的办法是,通过设置 PULSE_SERVER 环境变量以及加载 module-native-protocol-tcp 模块,将音频信号直接通过网络发送到宿主机上,一点也不卡!

by 依云 at November 20, 2020 09:04 AM

November 16, 2020

百合仙子

Python 小版本升级是怎么 break 已有项目的

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

近日,Arch Linux 终于开始升级到 Python 3.9 了。很多人认为 Python 小版本升级容易搞坏兼容性,导致项目无法在新的版本上运行。事实是这样的吗?我正好借着 Arch Linux 升级 3.9 的机会,分析一下打包过程中失败的项目到底是出了什么事。

需要说明的是,我仅大致地分析了打包的报错信息,不排除分析出错,或者有额外的问题没有被看见的情况。另外我是在打包过程中随机(arbitrarily)取样,并且排除了我不能确定问题所在的案例。

以下项目测试失败是和 Python 3.9 相关的。排序是按照项目开发者的无辜程度排序的。也就是说,排序越靠前的,我越是认为项目开发者是无辜的;而像「硬编码 Python 3.9 为未发布的版本」这种完全不 future-proof 的做法,现在坏掉了真是自找的。

其中,使用的公开特性变化导致问题的有 3 个,调用私有属性或者方法、依赖非正式的文本信息的有 11 个,使用已废弃的特性的有 8 个,使用已被修复的 bug 的有 2 个,使用未来注定会出问题的信息的有 3 个。总共 27 个。

  • freecad: PyTypeObject.tp_print 没了
  • python-llfuse: PyTypeObject.tp_print 没了
  • linux-tools: PyMODINIT_FUNC 的变化导致了警告,然后被转为错误
  • python-blist: _PyObject_GC_IS_TRACKED不再在第三方库中可用(被公开 API 取代)
  • python-pyflakes: Python 语法解析报告的列位置似乎不太对,应该是受新的语法解析器的影响
  • python-pylint: Python 语法解析报告的列位置似乎不太对,应该是受新的语法解析器的影响
  • python-typing_inspect: 使用私有名称 typing._GenericAlias,结果新版本变成了 typing._SpecialGenericAlias
  • python-sphinx-autodoc-typehints: 看上去是类型标注相关的内部更改移除了 typing.Dict.__parameters__ 属性造成的
  • python-fastnumbers: 看上去是内部函数 _Py_dg_stdnan 不再被默认包含导致的问题
  • python-libcst: 类型标注相关的内部更改移除了 typing.Dict.__args__ 属性造成的
  • monkeytype: typing.Dict 的类型从 type 变成了 typing._SpecialGenericAlias
  • scrapy: 由于 typing.Optional[str] 的字符串表示由 typing.Union[str, NoneType] 变成了 typing.Optional[str] 导致 mitmproxy 运行出错,进而使得 scrapy 的测试失败
  • python-billiard: 调用的私有方法 _posixsubprocess.fork_exec 参数发生了变化
  • python-pytest-benchmark: argparse 的帮助信息格式有优化
  • python-opentracing: 自 3.7 起废弃的 asyncio.Task.current_task 被移除
  • python-engineio: 自 3.7 起废弃的 asyncio.Task.all_tasks 被移除
  • impacket: 自 3.2 起废弃的 array.array.tostring() 被移除
  • python-pybtex: 自 3.2 起废弃的 xml.etree.ElementTree.Element.getchildren 被移除
  • python-jsonpickle: 自 3.1 起废弃的 base64.decodestring 被移除
  • python-ioflo: 自 3.1 起废弃的 json.loads() 参数 encoding 被移除
  • routersploit: 自 Python 3 起废弃的 threading.Thread.isAlive 终于被移除了
  • python-socketpool: 自 Python 3 起废弃的 threading.Thread.isAlive 终于被移除了
  • python-furl: Python 3.9 修正了一处 URL 解析 bug
  • python-stem: Python 3.9 移除了错误的 unittest.mock.__version__
  • python-natsort: Python 的 Unicode 支持更新到了 13.0.0 版本,CHORASMIAN NUMBER ONE 字符被判定为数字,但是测试代码不认识,认为程序出错
  • python-pony: 对新版本的 Python 报不支持的错误
  • python-dephell-pythons: 硬编码 Python 3.9 为未发布的版本,但现在 3.9 已经发布了

而以下项目的测试失败与 Python 3.9 没有直接关系,共 26 个。其中与 Python 生态有关的有 18 个,与其它项目有关的有 4 个,依赖外部信息的有 3 个,包括一个特别搞笑的依赖夏令时是否生效的。

  • python-eventlet: 调用的 dnspython 私有方法已不存在;DNS 解析超时
  • python-markdown2: 语法高亮的结果有少许变化,不符合预期。推测是 pygments 新版本的变化
  • python-flake8-typing-imports: 似乎是 flake8 能够检测到更多的问题了
  • python-babel: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pygal: pytest 6.1.0 移除了 Metafunc 的 funcargnames 属性
  • python-flask-gravatar: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pytest-relaxed: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pytest-randomly 使用了已废弃的特性,测试被 pytest 拒绝
  • python-deprecated: 测试所预期的警告文本信息已经发生变化
  • python-dbus-signature-pyparsing: 执行时间超过了测试设定的 200ms 时限
  • python-tinycss2: flake8 风格检查未通过
  • python-pytest-runner: black 风格检查未通过
  • python-portend: black 风格检查未通过
  • python-aiohttp: @coroutine 的 DeprecationWarning 被视作错误
  • python-poetry: poetry-core 的一项数据由 dict 改为 OrderedDict,使得输出顺序与测试预期的不一致
  • python-isort: 将使用旧版本 isort 的外部项目的 import 排序视为正确,然后它还真出错了
  • python-cachecontrol: Python 2.7 相关
  • python-zc.lockfile: 测试代码把 Python 3 代码喂给了 Python 2.7。可能是该库已经不支持 2.7 了
  • python-occ-core: 依赖 OpenCASCADE 的版本更新,不被支持
  • protobuf: C 整型比较因表示范围问题而恒为假,警告转错误。是因为新版本的 gcc 比较聪明么?
  • gnome-passwordsafe: 构建系统发现有依赖缺失
  • io: C 代码引用了不存在的系统头文件
  • ceph: C++ 相关问题
  • python-distlib: 调用远程 XML-RPC 太多被限制导致预期的数据与实际错位
  • python-requests-toolbelt: 测试所需要的 HTTP 资源 404 了
  • postgresql: 夏令时结束,导致实际时区与预期对不上。「所以冬天就不要滚包啦,冬天要冬眠!」

所以在这些升级 Python 3.9 的项目中,不兼容 Python 3.9 仅仅只占一半,其中又有一半多属于「总有一天会坏掉」的类型(一大半属于「不听话」,使用没有明确文档、预期为私有的特性,少数尝试当预言家但是失败了)。最后剩下的,再一大半是使用了至少两个版本前已经说了要废弃的特性,只有三个莫名地发现自己真的被 Python 坑了,还都是 C API 部分的。

所以我对我自己的脚本顺利升级到 Python 3.9 非常有信心呢。可能有些老代码使用了已经废弃的特性,所以我也设置了环境变量 PYTHONWARNINGS=default,ignore::ResourceWarning 以便及时得到提示。

哦对了,Arch Linux 中受 Python 3.9 升级影响需要更新的软件包共有2077个,绝大部分我都没见着失败的。目前从开始升级到现在已经过去六天,还剩最后40个失败了的包。

by 依云 at November 16, 2020 01:54 PM

November 02, 2020

中文社区新闻

无障碍(accessible)安装媒介

我们高兴地宣布从 archiso v49 开始我们的安装媒介集成了无障碍功能(accessibility)支持。从 2020.11.01 起,可以从发布的安装媒介中的第二项启动项开启这个特性。在 wiki 上有关于这个的特殊安装指引页

非常感谢 Alexander Epaneshnikov 从 TalkingArch 项目中将相关特性集成到 archiso 的 releng 设置中,我们用它来创建安装媒介。

注意:引导器的超时设置改到了15秒,方便盲人用户选择引导项,因为引导器本身没有提供可用性的相关支持。

by farseerfc at November 02, 2020 12:44 AM

October 29, 2020

百合仙子

让 Arch Linux 系统和最新的镜像同步,从最快的镜像下载

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

Arch Linux 就是要追新!要追新自然要选择一个更新及时的软件仓库镜像啦,比如国内的 TUNA、USTC 同步都很及时。但是呢,这俩难兄难弟最近一段时间有些吃不消了,导致下载包的时候很慢,甚至超时失败,使用体验真糟糕。如果直接用上游镜像,比如 pkgbuild.com,漂洋过海的,也挺慢的。

而国内另一些镜像,比如网易腾讯云阿里云华为云,他们要么有 CDN,要么线路很好,下载速度飞快。但是呢,他们基本上每天才同步一次,阿里云还时不时连续数天都没能同步成功,这让喜欢追新的 Arch Linux 用户多不舒服呀。当群里的小伙伴们都用上了最新版本的软件,体会到了让人心痒痒的新特性和 bug 时,你 -Syu 却是「今日无事可做」,真是扫兴呢。

和最新的镜像同步,从最快的镜像下载,真的不可兼得吗?

非也。只需要稍微配置一下,用上我的 pacsync 脚本,就可以啦~

配置方式是,为 /etc/pacman.d 下的镜像列表文件创建一个.sync后缀的同名文件,里边指定用于同步的镜像,而不带.sync后缀的文件里按优先级列出多个镜像。pacman 在下载文件时,会按顺序依次尝试列出的镜像,如果遇到更新不及时 404 的时候,就会尝试另一个。这样,可以仅在下载快的镜像里还没有需要的包文件时,才转而从比较慢的镜像下载。

而需要同步 pacman 数据库的时候,使用pacsync脚本取代pacman -Sy。脚本会使用 bind mount 用.sync文件取代不.sync的版本,就能同步到最新的数据库了。原来的pacman -Syu命令要拆开来用,先pacsyncpacman -Su了。

脚本里使用了单独的挂载空间并且将挂载改为了私有,所以并不会影响到外边。

by 依云 at October 29, 2020 02:31 PM

tar 归档的权限问题

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

一次系统升级之后,我的许多 Python 程序突然开始报错:

[...]
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 2762, in _get_metadata
    for line in self.get_metadata_lines(name):
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 1415, in get_metadata_lines
    return yield_lines(self.get_metadata(name))
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 1405, in get_metadata
    value = self._get(path)
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 1609, in _get
    with open(path, 'rb') as stream:
PermissionError: [Errno 13] Permission denied: '/usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/PKG-INFO'

WTF 滚坏了!立即回滚!

回滚完之后,我开始调查这个事件——因为 [archlinuxcn] 的这个包是我管的呀。而且我记得之前也遇到过一次类似的情况,当时没有深究。

检查一下软件包里的文件的权限:

>>> tar tvf python-telethon-1.17.4-1-any.pkg.tar.zst | grep PKG-INFO
-rw------- root/root      3659 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/PKG-INFO
>>> tar tvf python-telethon-1.17.4-1-any.pkg.tar.zst | grep -- ----
-rw------- root/root      3659 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/PKG-INFO
-rw------- root/root     12078 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/SOURCES.txt
-rw------- root/root         1 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/dependency_links.txt
-rw------- root/root        27 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/requires.txt
-rw------- root/root        15 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/top_level.txt

好奇怪,Telethon 这些包信息文件怎么只让 root 读了呢?

从 PyPI 上下载 Telethon 的原始 tar 归档回来看看,发现最近几个版本里,文件权限全部只有自己可以读(-rw-------),而所有者是 u0_a167/10167。开发者突然在 Android 系统上打包了呢……安装的时候,部分文件的权限被保留了下来(Arch Linux 打包时强烈反对使用 root 权限执行,因此我用 devtools 打包,解包部分自然是普通用户操作的,所有者无法被保留)。

然后我又看了一下之前的版本,哦豁,所有者成开发者的 id 了,但是有三个版本的 pyc 文件,还有好几个 pyc 文件都是 -rwxrwxrwx。大概系统上的低权限用户可以去改改,然后看谁跑 Telegram 机器人就拿谁的权限?

经过跟开发者的讨论,最终干掉了 pyc 文件,也不在 Android 上打包了。777 权限问题还待解决。不过我更在意的是,为什么会发生这种状况呢?setuptools 干嘛不修一修呢?别的工具创建的用于发布的 tar 归档会不会有类似的问题呢?

结果找了找,发现 setuptools 前年就有人报告这个问题,但是并没有解决。行吧,我打包时统一修正一下权限好了……

下一个 GitHub 生成的 tar 归档看看?咦,-rw-rw-r-- root/root,是处理过了么?啊对,git archive 生成的包是怎么样的?去试了试,原来一样的啊。看来 git 想到了这个问题并且处理了,只是 002 的 umask 有点意外。

Arch Linux 为了普通用户打出文件为 root 所有的 tar 归档使用了 fakeroot,那么 git 是怎么实现的呢?翻了翻代码,git 是自己生成 tar 文件的,写死了所有者是 root/root,但是权限位还是有专门的 umask,默认是 002。可以配置,比如git config --global tar.umask user一下,就会取当前 umask 作为 tar 归档里文件的 umask 了。

至于传统的 GNU autotools 构建系统创建的 tar 归档,我也创建了一个看了一下,并没有特殊处理,跟手动跑 tar 一样。

by 依云 at October 29, 2020 02:01 PM

October 27, 2020

berberman

Setting up a Haskell development environment on Arch Linux

Posted on October 27, 2020 by berberman

Once you accept the principles of Arch Linux – being simplicity and modernity – everything goes easier. In this article, we will use up-to-date Haskell ecosystem by using system provided Haskell packages, getting rid of awkward stack which could eat huge amount of your disk space. We won’t going to nix or ghcup, since they are both general Haskell toolchain solutions, not specific to Arch Linux.

Preface

If you get pandoc, shellcheck, or other Haskell programs installed on your system, you will find that a bunch of packages with haskell- prefix emerge frequently when rolling the system, which is pretty verbose and noisy. Thus, many of general users, i.e. not Haskell developers, always complain that “why every time I tries to roll my system, there are so many Haskell packages to be updated, and wait… What Is Haskell?” Remember that Arch Linux official repositories are not built for Haskell developers, haskell- packages distributed there are only for programs written in Haskell. To save disk space, Haskell executables do not bundle their dependencies, so libraries are stripped into independent packages, consistent with Haskell package management. Other languages’ distributions follow the similar strategy, whereas a vexing problem arise particularly in Haskell packaging. Haskell packages are packaged and linked dynamically, but GHC does not provide a stable ABI, since its specific hash method acting on circular dependences, which lie ubiquitously involving tests, will cause the soname of shared libraries interdependent. Consequently, you may notice that the entire Haskell packages in [community] are not reproducible. If a Haskell library changes, all dependent packages are required to be rebuilt. For us, it is inevitable to rebuild and reinstall all tools which depend on those shared libraries after updating haskell- packages. Some users choose to avoid getting involved this sort of cheating, using static version Haskell programs as alternative. However, as a Haskell developer, we can make full use of these shared libraries.

Configure Cabal

We will use Cabal without sandboxes as our build tool, and system level GHC as our compiler. Let’s install them via system package management tool:

# pacman -S ghc cabal-install

Generate the configuration and update hackage index as normal:

$ cabal update

Cabal are able to find system Haskell packages installed by pacman. If you try to use Cabal directly to compiling your project now, you will get:

Could not find module ‘Prelude’…

indicating that many packages which would come with GHC are missing. This is because currently system Haskell packages provide only dynamic linked shared libraries, which can be used only when GHC is running in dynamic. So we have to configure ~/.cabal/config as following:

library-vanilla: False
shared: True
executable-dynamic: True
program-default-options
  ghc-options: -dynamic

And if we want to install a Haskell program from cabal, we have to run:

cabal install --ghc-options=-dynamic [package to install]

to let Cabal call GHC enabled dynamic linking.

Install the language server

Personally, I would recommend haskell-language-server, which is active in developing and provides unprecedented coding experience. Because we choose using dynamic GHC, we have to compile HLS by ourselves with dynamic option. Clone the source code:

$ git clone https://github.com/haskell/haskell-language-server --recurse-submodules
$ cd haskell-language-server

IMPORTANT: Configure the HLS project locally:

$ cabal configure --disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic

Finally install it:

$ ./cabal-hls-install latest

Next step is choosing your favorite editor, and installing following the instruction in HLS. That’s it, happy coding!

Conclusion

Indeed, we have encountered the first impediment in installing HLS. Using dynamic GHC with system Haskell packages is double-edged, suggesting that we have to face various lurking issues.

Pros:

  • it’s impossible to get stuck into dependency hell (we always use the latest Haskell packages)
  • far less disk usage is required

Cons:

  • out-of-date packages are not available (so there will be slightly fewer libraries we can use)
  • programs involving GHC should be given special treatment (make sure GHC is called with dynamic flag)

The last is kind of troublesome, because maintainers should patch the source of those programs to let them call GHC properly. Here are some examples:

Overall, it seems that we’d better don’t touch these dynamic things in Haskell developing… Anyway, I’m posting this to illustrate that working with haskell- packages is possible. It is worth mentioning that all Haskell packages, and even three tenths of entire Arch Linux packages are maintaining by felixonmars individually, who is dedicated to these unpaid contributions. As an Arch Linux user, I would like to express my high respects to his greatest professionalism and responsibility. Next time, I will introduce my Haskell packaging tool arch-hs, which can be used by both Arch Linux Haskell packagers and Haskell developers.

October 27, 2020 12:00 AM

Setting up a Haskell development environment on Arch Linux

Posted on October 27, 2020 by berberman

Once you accept the principles of Arch Linux – being simplicity and modernity – everything goes easier. In this article, we will use up-to-date Haskell ecosystem by using system provided Haskell packages, getting rid of awkward stack which could eat huge amount of your disk space. We won’t going to nix or ghcup, since they are both general Haskell toolchain solutions, not specific to Arch Linux.

Preface

If you get pandoc, shellcheck, or other Haskell programs installed on your system, you will find that a bunch of packages with haskell- prefix emerge frequently when rolling the system, which is pretty verbose and noisy. Thus, many of general users, i.e. not Haskell developers, always complain that “why every time I tries to roll my system, there are so many Haskell packages to be updated, and wait… What Is Haskell?” Remember that Arch Linux official repositories are not built for Haskell developers, haskell- packages distributed there are only for programs written in Haskell. To save disk space, Haskell executables do not bundle their dependencies, so libraries are stripped into independent packages, consistent with Haskell package management. Other languages’ distributions follow the similar strategy, whereas a vexing problem arise particularly in Haskell packaging. Haskell packages are packaged and linked dynamically, but GHC does not provide a stable ABI, since its specific hash method acting on circular dependences, which lie ubiquitously involving tests, will cause the soname of shared libraries interdependent. Consequently, you may notice that the entire Haskell packages in [community] are not reproducible. If a Haskell library changes, all dependent packages are required to be rebuilt. For us, it is inevitable to rebuild and reinstall all tools which depend on those shared libraries after updating haskell- packages. Some users choose to avoid getting involved this sort of cheating, using static version Haskell programs as alternative. However, as a Haskell developer, we can make full use of these shared libraries.

Configure Cabal

We will use Cabal without sandboxes as our build tool, and system level GHC as our compiler. Let’s install them via system package management tool:

# pacman -S ghc cabal-install

Generate the configuration and update hackage index as normal:

$ cabal update

Cabal are able to find system Haskell packages installed by pacman. If you try to use Cabal directly to compiling your project now, you will get:

Could not find module ‘Prelude’…

indicating that many packages which would come with GHC are missing. This is because currently system Haskell packages provide only dynamic linked shared libraries, which can be used only when GHC is running in dynamic. So we have to configure ~/.cabal/config as following:

library-vanilla: False
shared: True
executable-dynamic: True
program-default-options
  ghc-options: -dynamic

And if we want to install a Haskell program from cabal, we have to run:

cabal install --ghc-options=-dynamic [package to install]

to let Cabal call GHC enabled dynamic linking.

Install the language server

Personally, I would recommend haskell-language-server, which is active in developing and provide unprecedented coding experience. Because we choose using dynamic GHC, we have to compile HLS by ourselves with dynamic option. Clone the source code:

$ git clone https://github.com/haskell/haskell-language-server --recurse-submodules
$ cd haskell-language-server

IMPORTANT: Configure the HLS project locally:

$ cabal configure --disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic

Finally install it:

$ ./cabal-hls-install latest

Next step is choosing your favorite editor, and installing following the instruction in HLS. That’s it, happy coding!

Conclusion

Indeed, we have encountered the first impediment in installing HLS. Using dynamic GHC with system Haskell packages is double-edged, suggesting that we have to face various lurking issues.

Pros:

  • it’s impossible to get stuck into dependency hell (we always use the latest Haskell packages)
  • far less disk usage is required

Cons:

  • out-of-date packages are not available (so there will be slightly fewer libraries we can use)
  • programs involving GHC should be given special treatment (make sure GHC is called with dynamic flag)

The last is kind of troublesome, because maintainers should patch the source of those programs to let them call GHC properly. Here are some examples:

Overall, it seems that we’d better don’t touch these dynamic things in Haskell developing… Anyway, I’m posting this to illustrate that working with haskell- packages is possible. It is worth mentioning that all Haskell packages, and even a half of entire Arch Linux packages are maintaining by felixonmars individually, who is dedicated to these unpaid contributions. As an Arch Linux user, I would like to express my high respects to his greatest professionalism and responsibility. Next time, I will introduce my Haskell packaging tool arch-hs, which can be used by both Arch Linux Haskell packagers and Haskell developers.

October 27, 2020 12:00 AM

October 24, 2020

中文社区新闻

libtraceevent>=5.9-1 升级需要手动干预

libtraceevent 包在版本 5.9-1 之前缺失了一个动态库链接。这个问题已经在 5.9-1 中修复,所以更新时需要覆盖 ldconfig 创建出的未被跟踪到的文件。如果你在升级时遇到如下报错:

libtraceevent: /usr/lib/libtraceevent.so.1 exists in filesystem

那么请使用命令:

pacman -Syu --overwrite /usr/lib/libtraceevent.so.1

完成更新。

by farseerfc at October 24, 2020 03:21 AM

October 21, 2020

中文社区新闻

nvidia 455.28 与 linux >= 5.9 不兼容

当前的 nvidia 与 linux >= 5.9 内核部分程度不兼容 [1] [2] 。虽然图形显示应该还能工作,但是搞坏了 CUDA 、 OpenCL 以及可能还有别的一些特性。建议需要这些特性而已经升级了内核的用户切换到 linux-lts 内核,直到 nvidia 提供修复。

by farseerfc at October 21, 2020 04:46 AM

October 06, 2020

Alynx Zhou

奇怪的书名和我和吐槽

好久没更新非技术类博文了。可能很多人不但丢掉了阅读的习惯,连书店都不去了,书店都沦为练习册店,学生们更加对书店敬而远之,这种恶性循环实在是可悲又无趣的事情。

今天和同学一起去了书店,在常规意义的书店而非练习册店的部分发现了一些有趣或奇怪的书名,很有槽点,让人想要吐槽。于是拍了点照片发到这里。

套娃型

此类书名非常适合套娃,比如这本《如何阅读一本书》,且不说阅读方法因人而异且随经验变化,单就这个名字还可以有《如何阅读如何阅读一本书》、《如何阅读如何阅读如何阅读一本书》。

如何阅读一本书

我以为只有当代为搏销量疯狂吸引眼球的作者会起这种名字,然而接下来我看到了一本《木心谈木心》。虽然我不太清楚木心是谁,但似乎是个有名的作者。这书名总是让我想后续会不会有《木心谈木心谈木心》、《木心谈木心谈木心谈木心》。

木心谈木心

性转型

这本书的名字非常奇特,她叫做《少女福尔摩斯》,作为半资深福尔摩斯迷和半资深月球氪金母X,我只能说型月都没敢做的这作者做了。由于原作实在是珠玉在前,写老福的故事已经很难了,性转老福怕是难上加难,建议寄一本给蘑菇看看。

少女福尔摩斯

蹭热度型

这位叫蒋勋的作者在书架上占据了一片,又是说唐诗又是说宋词,一会谈莫奈一会谈高达哦不是梵高和达芬奇,想必上知天文下知地理。不过这书脊把这些名人加粗放在下面,让我总是有一种这是个达芬奇/梵高/莫奈的著作、作品标题叫做蒋勋的感觉。 不知道这个叫蒋勋的作品是不是样样通样样松呢?

蒋勋

还有这本《逝水年华》,连我的输入法都怀疑我是不是想输入《追忆似水年华》,敢问作者是不是有点大舌头,让出版社的编辑听错了标题?

逝水年华

连锁反应型

《知更鸟》吗?大家都说有本书想杀你,不知道与你是什么关系?

知更鸟

虽然《重返美丽新世界》和《美丽新世界》是一个作者,但总让我想起狗尾续貂,不知道是不是我错了。

美丽新世界

好家伙,你把《24个比利》放在这,那他们肯定会打架的,不过叫《比利打群架》比《比利战争》更好吧,无谓夸大不可取,要实事求是。

比利

《面包树上的女人》怎么啦?她没怎么,《面包树出走了》,那可真是个离奇的故事,但是你这内容就不知道是不是好故事了。

面包树

我以为文学大家不屑于做这种在标题上吸引人的无聊事情,毕竟内容才是王道。我也不是有意冒犯杨绛先生,但是《洗澡》做书名已经是太随意了,再整一本《洗澡之后》是不是过分了?还是说这是出书的不负责任的编辑洗澡前洗澡后决定的两本文集的名字?那这就说得通了。但是说实话《洗澡》、《洗澡之后》接下来摆一本《暗示》是不是店员的恶趣味?

洗澡

希望看了我文章的朋友不要给我评论“一看到白胳膊……”,我老实承认我不是一个完全脱离了低级趣味的人,写这文章也是让大家开心一下,毕竟实事求是更重要。我也希望写书的作者们都实事求是一点,内容不行,靠标题党吸引人肯定是经不起考验的。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at October 06, 2020 09:48 AM

farseerfc

关于 swap 的一些补充

上周翻译完 【译】替 swap 辩护:常见的误解 之后很多朋友们似乎还有些疑问和误解,于是写篇后续澄清一下。事先声明我不是内核开发者, 这里说的只是我的理解, 基于内核文档中关于物理内存的描述 ,新的内核代码的具体行为可能和我的理解有所出入,欢迎踊跃讨论。

误解1: swap 是虚拟内存,虚拟内存肯定比物理内存慢嘛

这种误解进一步的结论通常是:「使用虚拟内存肯定会减慢系统运行时性能,如果物理内存足够为什么还要用虚拟的?」 这种误解是把虚拟内存和交换区的实现方式类比于「虚拟磁盘」或者「虚拟机」等同的方式, 也隐含「先用物理内存,用完了之后用虚拟内存」也即下面的「误解3」的理解。

首先,交换区(swap) 不是 虚拟内存。操作系统中说「物理内存」还是「虚拟内存」的时候在指程序代码 寻址时使用的内存地址方式,使用物理地址空间时是在访问物理内存,使用虚拟地址空间时是在访问虚拟内存。 现代操作系统在大部分情况下都在使用虚拟地址空间寻址, 包括 在执行内核代码的时候。

并且,交换区 不是 实现虚拟内存的方式。操作系统使用内存管理单元(MMU,Memory Management Unit)做虚拟内存地址到物理内存地址的地址翻译,现代架构下 MMU 通常是 CPU 的一部分,配有它专用的一小块存储区叫做地址转换旁路缓存(TLB,Translation Lookaside Buffer), 只有在 TLB 中没有相关地址翻译信息的时候 MMU 才会以缺页中断的形式调用操作系统内核帮忙。 除了 TLB 信息不足的时候,大部分情况下使用虚拟内存都是硬件直接实现的地址翻译,没有软件模拟开销。 实现虚拟内存不需要用到交换区,交换区只是操作系统实现虚拟内存后能提供的一个附加功能, 即便没有交换区,操作系统大部分时候也在用虚拟内存,包括在大部分内核代码中。

误解2: 但是没有交换区的话,虚拟内存地址都有物理内存对应嘛

很多朋友也理解上述操作系统实现虚拟内存的方式,但是仍然会有疑问:「我知道虚拟内存和交换区的区别, 但是没有交换区的话,虚拟内存地址都有物理内存对应,不用交换区的话就不会遇到读虚拟内存需要读写磁盘 导致的卡顿了嘛」。

这种理解也是错的,禁用交换区的时候,也会有一部分分配给程序的虚拟内存不对应物理内存, 比如使用 mmap 调用实现内存映射文件的时候。实际上即便是使用 read/​write 读写文件, Linux 内核中(可能现代操作系统内核都)在底下是用和 mmap 相同的机制建立文件 到虚拟地址空间的地址映射,然后实际读写到虚拟地址时靠缺页中断把文件内容载入页面缓存(page cache )。内核加载可执行程序和动态链接库的方式也是通过内存映射文件。甚至可以进一步说, 用户空间的虚拟内存地址范围内,除了匿名页之外,其它虚拟地址都是文件后备(backed by file ),而匿名页通过交换区作为文件后备。上篇文章中提到的别的类型的内存,比如共享内存页面(shm )是被一个内存中的虚拟文件系统后备的,这一点有些套娃先暂且不提。于是事实是无论有没有交换区, 缺页的时候总会有磁盘读写从慢速存储加载到物理内存,这进一步引出上篇文章中对于交换区和页面缓存这两者的讨论。

误解3: 不是内存快用完的时候才会交换的么?

简短的答案可以说「是」,但是内核理解的「内存快用完」和你理解的很可能不同。 也可以说「不是」,就算按照内核理解的「内存快用完」的定义,内存快用完的时候内核的行为是去回收内存, 至于回收内存的时候内核会做什么有个复杂的启发式经验算法,实际上真的内存快满的时候根本来不及做 swap ,内核可能会尝试丢弃 page cache 甚至丢弃 vfs cache (dentry cache / inode cache) 这些不需要磁盘I/O就能更快获取可用内存的动作。

深究这些内核机制之前,我在思考为什么很多朋友会问出这样的问题。可能大部分这么问的人,学过编程, 稍微学过基本的操作系统原理,在脑海里对内核分配页面留着这样一种印象(C伪代码):

////////////////////  userspace space  ////////////////
void* malloc(int size){
    void* pages = mmap(...);                                    // 从内核分配内存页
    return alloc_from_page(pages, size);                        // 从拿到的内存页细分
}

////////////////////  kernel space  //////////////////
void * SYSCALL do_mmap(...){
   //...
   return kmalloc_pages(nr_page);
}

void* kmalloc_pages(int size){
  while ( available_mem < size ) {
    // 可用内存不够了!尝试搞点内存
    page_frame_info* least_accessed = lru_pop_page_frame();     // 找出最少访问的页面
    switch ( least_accessed -> pf_type ){
      case PAGE_CACHE: drop_page_cache(least_accessed); break;  // 丢弃文件缓存
      case SWAP:       swap_out(least_accessed);        break;  // <- 写磁盘,所以系统卡了!
      // ... 别的方式回收 least_accessed
    }
    append_free_page(free_page_list, least_accessed);           // 回收到的页面加入可用列表
    available_mem += least_accessed -> size;
  }
  // 搞到内存了!返回给程序
  available_mem -= size;
  void * phy_addr = take_from_free_list(free_page_list, size);
  return assign_virtual_addr(phy_addr);
}

这种逻辑隐含三层 错误的 假设:

  1. 分配物理内存是发生在从内核分配内存的时候的,比如 malloc/​mmap 的时候。
  2. 内存回收是发生在进程请求内存分配的上下文里的,换句话说进程在等内核的内存回收返回内存, 不回收到内存,进程就得不到内存。
  3. 交换出内存到 swap 是发生在内存回收的时候的,会阻塞内核的内存回收,进而阻塞程序的内存分配。

这种把内核代码当作「具有特权的库函数调用」的看法,可能很易于理解, 甚至早期可能的确有操作系统的内核是这么实现的,但是很可惜现代操作系统都不是这么做的。 上面三层假设的错误之处在于:

  1. 在程序请求内存的时候,比如 malloc/​mmap 的时候,内核只做虚拟地址分配, 记录下某段虚拟地址空间对这个程序是可以合法访问的,但是不实际分配物理内存给程序。 在程序第一次访问到虚拟地址的时候,才会实际分配物理内存。这种叫 惰性分配(lazy allocation)
  2. 在内核感受到内存分配压力之后,早在内核内存用尽之前,内核就会在后台慢慢扫描并回收内存页。 内存回收通常不发生在内存分配的时候,除非在内存非常短缺的情况下,后台内存回收来不及满足当前分配请求, 才会发生 直接回收(direct reclamation)
  3. 同样除了直接回收的情况,大部分正常情况下换出页面是内存管理子系统调用 DMA 在后台慢慢做的, 交换页面出去不会阻塞内核的内存回收,更不会阻塞程序做内存分配(malloc )和使用内存(实际访问惰性分配的内存页)。

也就是说,现代操作系统内核是高度并行化的设计,内存分配方方面面需要消耗计算资源或者 I/O 带宽的场景,都会尽量并行化,最大程度利用好计算机所有组件(CPU/MMU/DMA/IO)的吞吐率, 不到紧要关头需要直接回收的场合,就不会阻塞程序的正常执行流程。

惰性分配有什么好处?

或许会有人问:「我让你分配内存,你给我分配了个虚拟的,到用的时候还要做很多事情才能给我,这不是骗人嘛」, 或者会有人担心惰性分配会对性能造成负面影响。

这里实际情况是程序从分配虚拟内存的时候,「到用的时候」,这之间有段时间间隔,可以留给内核做准备 。程序可能一下子分配一大片内存地址,然后再在执行过程中解析数据慢慢往地址范围内写东西。 程序分配虚拟内存的速率可以是「突发」的,比如一个系统调用中分配 1GiB 大小,而实际写入数据的速率会被 CPU 执行速度等因素限制,不会短期内突然写入很多页面。 这个分配速率导致的时间差内内核可以完成很多后台工作,比如回收内存, 比如把回收到的别的进程用过的内存页面初始化为全0,这部分后台工作可以和程序的执行过程并行, 从而当程序实际用到内存的时候,需要的准备工作已经做完了,大部分场景下可以直接分配物理内存出来。

如果程序要做实时响应,想避免因为惰性分配造成的性能不稳定,可以使用 mlock/​mlockall 将得到的虚拟内存锁定在物理内存中,锁的过程中内核会做物理内存分配。不过要区分「性能不稳定」和「低性能」, 预先分配内存可以避免实际使用内存时分配物理页面的额外开销,但是会拖慢整体吞吐率,所以要谨慎使用。

很多程序分配了很大一片地址空间,但是实际并不会用完这些地址,直到程序执行结束这些虚拟地址也一直 处于没有对应物理地址的情况。惰性分配可以避免为这些情况浪费物理内存页面,使得很多程序可以无忧无虑地 随意分配内存地址而不用担心性能损失。这种分配方式也叫「超额分配(overcommit)」。飞机票有超售, VPS 提供商划分虚拟机有超售,操作系统管理内存也同样有这种现象,合理使用超额分配能改善整体系统效率。

内核要高效地做到惰性分配而不影响程序执行效率的前提之一,在于程序真的用到内存的时候, 内核能不做太多操作就立刻分配出来,也就是说内核需要时时刻刻在手上留有一部分空页, 满足程序执行时内存分配的需要。换句话说,内核需要早在物理内存用尽之前,就开始回收内存。

那么内核什么时候会开始回收内存?

首先一些背景知识:物理内存地址空间并不是都平等,因为一些地址范围可以做 DMA 而另一些不能,以及 NUMA 等硬件环境倾向于让 CPU 访问其所在 NUMA 节点内存范围。在 32bit 系统上内核的虚拟地址空间还有低端内存和高端内存的区分,他们会倾向于使用不同属性的物理内存,到 64bit 系统上已经没有了这种限制。

硬件限制了内存分配的自由度,于是内核把物理内存空间分成多个 Zone ,每个 Zone 内各自管理可用内存, Zone 内的内存页之间是相互平等的。

zone 内水位线
ditaa diagram

一个 Zone 内的页面分配情况可以右图描绘。 除了已用内存页,剩下的就是空闲页(free pages),空闲页范围中有三个水位线(watermark )评估当前内存压力情况,分别是高位(high)、低位(low)、最小位(min)。

当内存分配使得空闲页水位低于低位线,内核会唤醒 kswapd 后台线程, kswapd 负责扫描物理页面的使用情况并挑选一部分页面做回收,直到可用页面数量恢复到水位线高位(high)以上。 如果 kswapd 回收内存的速度慢于程序执行实际分配内存的速度, 可用空闲页数量可能进一步下降,降至低于最小水位(min)之后,内核会让内存分配进入 直接回收(direct reclamation) 模式,在直接回收模式下,程序分配某个物理页的请求( 第一次访问某个已分配虚拟页面的时候)会导致在进程上下文中阻塞式地调用内存回收代码。

除了内核在后台回收内存,进程也可以主动释放内存,比如有程序退出的时候就会释放一大片内存页, 所以可用页面数量可能会升至水位线高位以上。有太多可用页面浪费资源对整体系统运行效率也不是好事, 所以系统会积极缓存文件读写,所有 page cache 都留在内存中,直到可用页面降至低水位以下触发 kswapd 开始工作。

设置最小水位线(min)的原因在于,内核中有些硬件也会突然请求大量内存,比如来自网卡接收到的数据包, 预留出最小水位线以下的内存给内核内部和硬件使用。

设置高低两个控制 kswapd 开关的水位线是基于控制理论。唤醒 kswapd 扫描内存页面本身有一定计算开销,于是每次唤醒它干活的话就让它多做一些活( high - low ),避免频繁多次唤醒。

因为有这些水位线,系统中根据程序请求内存的「速率」,整个系统的内存分配在宏观的一段时间内可能处于以下几种状态:

  1. 不回收: 系统中的程序申请内存速度很慢,或者程序主动释放内存的速度很快, (比如程序执行时间很短,不怎么进行文件读写就马上退出,)此时可用页面数量可能一直处于低水位线以上, 内核不会主动回收内存,所有文件读写都会以页面缓存的形式留在物理内存中。
  2. 后台回收: 系统中的程序在缓慢申请内存,比如做文件读写, 比如分配并使用匿名页面。系统会时不时地唤醒 kswapd 在后台做内存回收, 不会干扰到程序的执行效率。
  3. 直接回收: 如果程序申请内存的速度快于 kswapd 后台回收内存的速度, 空闲内存最终会跌破最小水位线,随后的内存申请会进入直接回收的代码路径,从而极大限制内存分配速度。 在直接分配和后台回收的同时作用下,空闲内存可能会时不时回到最小水位线以上, 但是如果程序继续申请内存,空闲内存量就会在最小水位线附近上下徘徊。
  4. 杀进程回收: 甚至直接分配和后台回收的同时作用也不足以拖慢程序分配内存的速度的时候, 最终空闲内存会完全用完,此时触发 OOM 杀手干活杀进程。

系统状态处于 1. 不回收 的时候表明分配给系统的内存量过多,比如系统刚刚启动之类的时候。 理想上应该让系统长期处于 2. 后台回收 的状态,此时最大化利用缓存的效率而又不会因为内存回收 减缓程序执行速度。如果系统引导后长期处于 1. 不回收 的状态下,那么说明没有充分利用空闲内存做 文件缓存,有些 unix 服务比如 preload 可用来提前填充文件缓存。

如果系统频繁进入 3. 直接回收 的状态,表明在这种工作负载下系统需要减慢一些内存分配速度, 让 kswapd 有足够时间回收内存。就如前一篇翻译中 Chris 所述,频繁进入这种状态也不一定代表「内存不足」,可能表示内存分配处于非常高效的利用状态下, 系统充分利用慢速的磁盘带宽,为快速的内存缓存提供足够的可用空间。 直接回收 是否对进程负载有负面影响要看具体负载的特性。 此时选择禁用 swap 并不能降低磁盘I/O,反而可能缩短 2. 后台回收 状态能持续的时间, 导致更快进入 4. 杀进程回收 的极端状态。

当然如果系统长期处于 直接回收 的状态的话,则说明内存总量不足,需要考虑增加物理内存, 或者减少系统负载了。如果系统进入 4. 杀进程回收 的状态,不光用空间的进程会受影响, 并且还可能导致内核态的内存分配受影响,产生网络丢包之类的结果。

微调内存管理水位线

可以看一下运行中的系统中每个 Zone 的水位线在哪儿。比如我手上这个 16GiB 的系统中:

$ cat /proc/zoneinfo
Node 0, zone      DMA
   pages free     3459
         min      16
         low      20
         high     24
         spanned  4095
         present  3997
         managed  3975
Node 0, zone    DMA32
   pages free     225265
         min      3140
         low      3925
         high     4710
         spanned  1044480
         present  780044
         managed  763629
Node 0, zone   Normal
   pages free     300413
         min      13739
         low      17173
         high     20607
         spanned  3407872
         present  3407872
         managed  3328410

因为不是 NUMA 系统,所以只有一个 NUMA node,其中根据 DMA 类型共有 3 个 Zone 分别叫 DMA, DMA32, Normal 。三个 Zone 的物理地址范围(spanned)加起来大概有 \(4095+1044480+3407872\) 大约 17GiB 的地址空间,而实际可访问的地址范围(present )加起来有 \(3997+780044+3407872\) 大约 16GiB 的可访问物理内存。

其中空闲页面有 \(3459+762569+1460218\) 大约 8.5GiB ,三条水位线分别在: \(\texttt{high} = 24+4710+20607 = 98\texttt{MiB}\)\(\texttt{low} = 20+3925+17173 = 82\texttt{MiB}\)\(\texttt{min} = 16+3140+13739 = 65\texttt{MiB}\) 的位置。

具体这些水位线的确定方式基于几个 sysctl 。首先 min 基于 vm.min_free_kbytes 默认是基于内核低端内存量的平方根算的值,并限制到最大 64MiB 再加点余量,比如我这台机器上 vm.min_free_kbytes = 67584 ,于是 min 水位线在这个位置。 其它两个水位线基于这个计算,在 min 基础上增加总内存量的 vm.watermark_scale_factor /​ 10000 比例(在小内存的系统上还有额外考虑),默认 vm.watermark_scale_factor = 10 在大内存系统上意味着 low 比 min 高 0.1% , high 比 low 高 0.1% 。

可以手动设置这些值,以更早触发内存回收,比如将 vm.watermark_scale_factor 设为 100:

$ echo 100 | sudo tee /proc/sys/vm/watermark_scale_factor
$ cat /proc/zoneinfo
Node 0, zone      DMA
   pages free     3459
         min      16
         low      55
         high     94
         spanned  4095
         present  3997
         managed  3975
   Node 0, zone    DMA32
   pages free     101987
         min      3149
         low      10785
         high     18421
         spanned  1044480
         present  780044
         managed  763629
   Node 0, zone   Normal
   pages free     61987
         min      13729
         low      47013
         high     80297
         spanned  3407872
         present  3407872
         managed  3328410

得到的三条水位线分别在 \(\texttt{min} = 16+3149+13729 = 66\texttt{MiB}\)\(\texttt{low} = 55+10785+47013 = 226\texttt{MiB}\)\(\texttt{high} = 94+18421+80297 = 386\texttt{MiB}\) , 从而 low 和 high 分别比 min 提高 160MiB 也就是内存总量的 1% 左右。

在 swap 放在 HDD 的系统中,因为换页出去的速度较慢,除了上篇文章说的降低 vm.swappiness 之外,还可以适当提高 vm.watermark_scale_factor 让内核更早开始回收内存,这虽然会稍微降低缓存命中率,但是另一方面可以在进入直接回收模式之前 有更多时间做后台换页,也将有助于改善系统整体流畅度。

只有 0.1% ,这不就是说内存快用完的时候么?

所以之前的「误解3」我说答案可以说「是」或者「不是」,但是无论回答是或不是,都代表了认为「swap 就是额外的慢速内存」的错误看法。当有人在强调「swap 是内存快用完的时候才交换」的时候, 隐含地,是在把系统总体的内存分配看作是一个静态的划分过程:打个比方这就像在说,我的系统里存储空间有快速 128GiB SSD 和慢速 HDD 的 1TiB ,同样内存有快速的 16GiB RAM 和慢速 16GiB 的 swap 。 这种静态划分的类比是错误的看待方式,因为系统回收内存进而做页面交换的方式是动态平衡的过程, 需要考虑到「时间」和「速率」而非单纯看「容量」。

假设 swap 所在的存储设备可以支持 5MiB/s 的吞吐率( HDD 上可能更慢, SSD 上可能更快,这里需要关注数量级),相比之下 DDR3 大概有 10GiB/s 的吞吐率,DDR4 大概有 20GiB/s ,无论多快的 SSD 也远达不到这样的吞吐(可能 Intel Optane 这样的 DAX 设备会改变这里的状况)。从而把 swap 当作慢速内存的视角来看的话,加权平均的速率是非常悲观的,「 16G 的 DDR3 + 16G 的 swap 会有 \(\frac{16 \times 10 \times 1024 + 16 \times 5}{16+16} = 5 \texttt{GiB/s}\) 的吞吐?所以开 swap 导致系统速度降了一半?」显然不能这样看待。

动态的看待方式是, swap 设备能提供 5MiB/s 的吞吐,这意味着:如果我们能把未来 10 分钟内不会访问到的页面换出到 swap ,那么就相当于有 \(10 \times 60 \texttt{s} \times 5 \texttt{MiB/s} = 3000 \texttt{MiB}\) 的额外内存,用来放那 10 分钟内可能会访问到的页面缓存。 10 分钟只是随口说的一段时间,可以换成 10 秒或者 10 小时,重要的是只要页面交换发生在后台, 不阻塞前台程序的执行,那么 swap 设备提供的额外吞吐率相当于一段时间内提供了更大的物理内存, 总是能提升页面缓存的命中,从而改善系统性能。

当然系统内核不能预知「未来 10 分钟内需要的页面」,只能根据历史上访问内存的情况预估之后可能会访问的情况, 估算不准的情况下,比如最近10分钟内用过的页面缓存在之后10分钟内不再被使用的时候, 为了把最近这10分钟内访问过的页面留在物理内存中,可能会把之后10分钟内要用到的匿名页面换出到了交换设备上。 于是会有下面的情况:

但是我开了 swap 之后,一旦复制大文件,系统就变卡,不开 swap 不会这样的

大概电脑用户都经历过这种现象,不限于 Linux 用户,包括 macOS 和 Windows 上也是。 在文件管理器中复制了几个大文件之后,切换到别的程序系统就极其卡顿,复制已经结束之后的一段时间也会如此。 复制的过程中系统交换区的使用率在上涨,复制结束后下降,显然 swap 在其中有重要因素,并且禁用 swap 或者调低 swappiness 之后就不会这样了。于是网上大量流传着解释这一现象,并进一步建议禁用 swap 或者调低 swappiness 的文章。我相信不少关心系统性能调优的人看过这篇「 Tales from responsivenessland: why Linux feels slow, and how to fix that 」或是它的转载、翻译,用中文搜索的话还能找到更多错误解释 swappiness 目的的文章,比如 这篇将 swappiness 解释成是控制内存和交换区比例的参数

除去那些有技术上谬误的文章,这些网文中描述的现象是有道理的,不单纯是以讹传讹。 桌面环境中内存分配策略的不确定性和服务器环境中很不一样,复制、下载、解压大文件等导致一段时间内 大量占用页面缓存,以至于把操作结束后需要的页面撵出物理内存,无论是交换出去的方式还是以丢弃页面缓存的方式, 都会导致桌面响应性降低。

不过就像前文 Chris 所述,这种现象其实并不能通过禁止 swap 的方式缓解:禁止 swap 或者调整 swappiness 让系统尽量避免 swap 只影响回收匿名页面的策略,不影响系统回收页面的时机, 也不能避免系统丢弃将要使用的页面缓存而导致的卡顿。

以前在 Linux 上也没有什么好方法能避免这种现象。 macOS 转用 APFS 作为默认文件系统之后, 从文件管理器(Finder)复制文件默认启用 file clone 快速完成,这操作不实际复制文件数据, 一个隐含优势在不需要读入文件内容,从而不会导致大量页面缓存失效。 Linux 上同样可以用支持 reflink 的文件系统比如 btrfs 或者开了 reflink=1 的 xfs 达到类似的效果。 不过 reflink 也只能拯救复制文件的情况,不能改善解压文件、下载文件、计算文件校验等情况下, 一次性处理大文件对内存产生的压力。

好在最近几年 Linux 有了 cgroup ,允许更细粒度地调整系统资源分配。进一步现在我们有了 cgroup v2 ,前面 Chris 的文章也有提到 cgroup v2 的 memory.low 可以某种程度上建议内存子系统 尽量避免回收某些 cgroup 进程的内存。

于是有了 cgroup 之后,另一种思路是把复制文件等大量使用内存而之后又不需要保留页面缓存的程序单独放入 cgroup 内限制它的内存用量,用一点点复制文件时的性能损失换来整体系统的响应流畅度。

关于 cgroup v1 和 v2

稍微跑题说一下 cgroup v2 相对于 v1 带来的优势。这方面优势在 Chris Down 另一个关于 cgroup v2 演讲 中有提到。老 cgroup v1 按控制器区分 cgroup 层级,从而内存控制器所限制的东西和 IO 控制器所限制的东西是独立的。在内核角度来看,页面写回(page writeback)和交换(swap)正是 夹在内存控制器和IO控制器管理的边界上,从而用 v1 的 cgroup 难以同时管理。 v2 通过统一控制器层级解决了这方面限制。具体见下面 Chris Down 的演讲。

用 cgroup v2 限制进程的内存分配

实际上有了 cgroup v2 之后,还有更多控制内存分配的方案。 cgroup v2 的内存控制器 可以对某个 cgroup 设置这些阈值:

  • memory.min : 最小内存限制。内存用量低于此值后系统不会回收内存。
  • memory.low : 低内存水位。内存用量低于此值后系统会尽量避免回收内存。
  • memory.high : 高内存水位。内存用量高于此值后系统会积极回收内存,并且会对内存分配节流(throttle)。
  • memory.max : 最大内存限制。内存用量高于此值后系统会对内存分配请求返回 ENOMEM,或者在 cgroup 内触发 OOM 。

可见这些设定值可以当作 per-cgroup 的内存分配水位线,作用于某一部分进程而非整个系统。 针对交换区使用情况也可设置这些阈值:

  • memory.swap.high : 高交换区水位,交换区用量高于此值后会对交换区分配节流。
  • memory.swap.max : 最大交换区限制,交换区用量高于此值后不再会发生匿名页交换。

到达这些 cgroup 设定阈值的时候,还可以设置内核回调的处理程序,从用户空间做一些程序相关的操作。

Linux 有了 cgroup v2 之后,就可以通过对某些程序设置内存用量限制,避免他们产生的页面请求把别的 程序所需的页面挤出物理内存。使用 systemd 的系统中,首先需要 启用 cgroup v2 ,在内核引导参数中加上 systemd.unified_cgroup_hierarchy=1 。然后开启用户权限代理:

# systemctl edit user@1000.service
[Service]
Delegate=yes

然后可以定义用户会话的 slice (slice 是 systemd 术语,用来映射 cgroup ),比如创建一个叫 limit-mem 的 slice :

$ cat ~/.config/systemd/user/limit-mem.slice
[Slice]
MemoryHigh=3G
MemoryMax=4G
MemorySwapMax=2G

然后可以用 systemd-run 限制在某个 slice 中打开一个 shell:

$ systemd-run --user --slice=limit-mem.slice --shell

或者定义一个 shell alias 用来限制任意命令:

$ type limit-mem
limit-mem is an alias for /usr/bin/time systemd-run --user --pty --same-dir --wait --collect --slice=limit-mem.slice
$ limit-mem cp some-large-file dest/

实际用法有很多,可以参考 systemd 文档 man systemd.resource-controlxuanwo有篇博客介绍过 systemd 下资源限制lilydjwg写过用 cgroup 限制进程内存的用法用 cgroup 之后对 CPU 调度的影响

未来展望

最近新版的 gnome 和 KDE 已经开始为桌面环境下用户程序的进程创建 systemd scope 了, 可以通过 systemd-cgls 观察到,每个通过桌面文件(.desktop)开启的用户空间程序 都有个独立的名字叫 app-APPNAME-HASH.scope 之类的 systemd scope 。 有了这些 scope 之后,事实上用户程序的资源分配某种程度上已经相互独立, 不过默认的用户程序没有施加多少限制。

今后可以展望,桌面环境可以提供用户友好的方式对这些桌面程序施加公平性的限制。 不光是内存分配的大小限制,包括 CPU 和 IO 占用方面也会更公平。 值得一提的是传统的 ext4/xfs/f2fs 之类的文件系统虽然支持 cgroup writeback 节流 但是因为他们有额外的 journaling 写入,难以单独针对某些 cgroup 限制 IO 写入带宽(对文件系统元数据的写入难以统计到具体某组进程)。 而 btrfs 通过 CoW 避免了 journaling , 在这方面有更好的支持 。相信不远的将来,复制大文件之类常见普通操作不再需要手动调用加以限制, 就能避免单个程序占用太多资源影响别的程序。

by farseerfc at October 06, 2020 04:45 AM

October 03, 2020

中文社区新闻

ghostpcl>=9.53.2-2 和 ghostxps>=9.53.2-2 升级需要手动干预

ghostpcl 和 ghostxps 包在版本 9.53.2-2 之前各缺失了一个动态库链接。这个问题已经在 9.53.2-2 中修复,所以更新时需要覆盖 ldconfig 创建出的未被跟踪到的文件。如果你在升级时遇到如下报错:

ghostpcl: /usr/lib/libgpcl6.so.9 exists in filesystem
ghostxps: /usr/lib/libgxps.so.9 exists in filesystem

那么请使用命令:

pacman -Syu --overwrite /usr/lib/libgpcl6.so.9,/usr/lib/libgxps.so.9

完成更新。

by farseerfc at October 03, 2020 03:25 PM