布局与输入法独立的可能性 a.k.a 我为什么在勉强使用 Fcitx

输入源管理的统一

前两年 GNOME 开始将 iBus 集成为其的一部分,为用户提供更为一致的输入体验,引发了众多非议,包括强迫用户使用如此之烂的 iBus “输入法”(显然是把 iBus 框架和 ibus-pinyin 引擎混为一谈的),阻止用户换用其它输入框架等等。

实际上,系统只通过唯一的框架来配置输入源(键盘和输入法)是非常有必要的。键盘布局是桌面环境管理的,而输入法却是独立的,那么听谁的?产生布局冲突问题。作为一个非 Qwerty 布局用户,经常遇到的问题是需要对 DE 和输入法都设置布局 —— 两个布局是独立的(这还算是好的情况);只对其中一个设置布局 —— 因为两者互相修改全局布局,导致只配置一个就 好,还时常遇到一切换输入法或开关输入引擎,布局就会被重设;或者布局被解码两次,负负得正等于布局没变化的问题。

尽管我所理想的输入体验是,时刻在键盘布局之间切换,而输入引擎(输入法)的工作不受其影响。然而连成功更换一个布局都如此之难,这也算是奢望了。如果布局和输入法彻底统一,那么以上那些如此烦人的问题都将不复存在。

键盘与键盘布局

“键盘”是泛指带有按键的输入设备,但大多数情况下,它是指遵照 ANSI 或 IEC-9995 规定的规格制造的输入设备。 在 IEC-9995 中,只对键盘按键的分布和样式做了强制规定,并没有强制规定拉丁字母如何在按键上分配。因此, 一个最简的 IEC-9995 键盘是一个无刻键盘(或至少字母区是无刻的),可以采用任何(至少是字母区)排列,即”键盘布局“。

键盘的每一个物理按键被触发后,会向电脑发送一个”按键扫描码“,是物理按键本身的唯一标识符。操作系统接收到以后可能会先做第一步映射,把扫描码映射成按键,比如把 19 映射到 ALPHA_1_4(名字可以随便取,但不幸的是,一般这个按键本身会用 Qwerty 的名字,比如 R);然后再把按键翻译成字符,例如 ALPHA_1_4 翻译成 R 或者 P,对于不同的键盘布局,可采用不同的映射表,这也是设置键盘布局的标准方法。

然而,由于这种映射并不总是能够被正确完美的实现,在一些情况下,更换键盘布局会采用其它的方式,例如,直接拦截并修改原始的键盘扫描码,这样其它程序依然认为它们在使用 Qwerty 布局而完全不用修改,这种方法甚至经常被固化在键盘本身,可以在任何操作系统下实现布局切换; 或者拦截字母,把字母按照表格进行修改,例如,字母 R 时就把它改成 P。

输入法 = 算法 or 键盘 + 算法?

对于惯用基于拉丁字母编码体系输入方法(包括但不限于某些中文和日文输入法)的用户来说,一个输入法要做的仅仅是把捕获的字符输入进行运算,得出运算结果显示在屏幕上,而不必理会字符和原始键盘按键的对应关系。这是正确的,无论我 是通过 Qwerty 键盘的 2 行 2 个输入 “s” 还是通过 Dvorak 键盘的同位置输入 “o”,输入法都捕获获得经过布局解析后的最终结果,而不能与键盘布局相关。

然而,这在其它输入方案里不适用,任何不基于拉丁字母的输入方案都对键盘上的每个物理按键赋予的其它的解释,也就是说,它同时是一个键盘布局和一个算法。例如在大千式注音输入方案中,键盘 1 行 1 个的按键用于输入“ㄆ”。

因此可见,对这个问题的正确处理方法,理应是让输入法本身获取拉丁字母输入法捕获键盘上原始按键扫描码,按照它们自己的理解进行翻译,而基于拉丁字母方案的输入法可以默认按照 Qwerty 解释,也能额外配置布局。加入自动遵照系统全局布局的特性就更好(但这里就引出了矛盾,见后文)。

然而,由于 Qwerty 布局被视作”键盘“而不是”键盘“的一种布局,因此输入法并不是按照如此方式,而假设系统发送过来的字母一定是通过 Qwerty 输入的,把字母当作按键扫描码来使用(即上一段的第二种方式),输入法反而依附在了 Qwerty 布局之上,而不是自己解析布局,对于非 Qwerty 布局就产生了混乱。

实际可行性

在大多数输入法框架中,键盘布局和某种输入法是不做区分,这在逻辑上也是正确的,毕竟布局和输入法都是输入方案,无论是 OS X,Windows 还是 iBus 或是 Fcitx 都对其这样处理。这样一来,任何输入法和键盘布局的确可以按照之前我所说完美方案工作。

但通常基于拉丁字母的输入法永远声称自己使用 Qwerty 布局,不提供任何额外的布局设置,这的确是一个可以改进的地方,目前,我只发现 Fcitx 可以这样做。而 Windows IME 只能通过 Hack 键盘扫描码来来更换布局,非常不便。

走到了这一步,依然没有实现我“一键切换布局,无论有没有输入法,布局就都变了”了需求。我在前文提到,这可以通过“自动遵照 系统全局布局”来解决,但这就引出了矛盾 —— 所有输入方法一律平等,如何遵守不存在的“全局布局”。为了做到这一点,我们就需要把键盘布局和输入法分离开,这就破坏了输入源平等的统一原则。而且,但更何况键盘不仅仅可以输入拉丁字母,还可以输入其它字母,比如斯拉夫字母,输入这些基本字母时不(一定)需要输入法的参与,这就让前面“键盘布局规定的是英文字母排列”的假设失效,因此,我们实际上是要设立一个“拉丁字母全局布局”的选项,我的天啊,这都什么玩意儿啊!

我的现状

因为 iBus 和 Windows IME 都一样无法满足我的需求,因此我也只好放弃了,虽然它们本身其实都不算坏。由于 Fcitx 有不错的特性:可以对输入法单独设置布局,而且还能让它们“遵守第一个输入布局”。因此我这个“一切换布局,无论有无输入法,都会改变布局”的需求算是实现了,但“一键切换布局”的特性就无法实现了,切换布局时,需要用一个新布局替换原有的第一布局“,因此算是”勉强“使用。不过这已经很不错了,Fcitx 也是个不错的框架,改天写个插件大概我的最终需求也能实现啦。

PGP 邮件自动加密方案

互联网安全随着以 NSA、GCHQ 为首的大规模政府监视已经变得岌岌可危了。Google 虽然还有一些信任度,但也不是像以前一样可以完全信任了。就说电子邮件吧,自建服务器才是目前最为可行的方案。然而,一旦服务器被入侵,上面的明文邮件也会全部泄露。

只有数学还是可信的。如果我们的邮件服务器在接收到邮件的同时,对邮件进行 PGP 加密再储存(已经加密就不需要再加密了),就是一个完美的安全方案了。如果这封邮件本身还是通过 TLS 加密的 SMTP 投递过来的,那么可以说牢不可破了。我们还可以把服务器配置成一个转发服务器,继续用自己原先的邮箱服务。

原本盖子打算写一个 Postfix 的 Filter 脚本来解决这个问题,然而,盖子发现几年前,就有人编写了 Exim 和 qpsmtpd 的插件支持,都是 Perl,在此分享:

注意,这会导致垃圾邮件也会被加密。一定要配合完善的反垃圾措施使用,qpsmtpd 的反垃圾就非常强大。

GPG 密钥更换

pub rsa4096/717E3FB8 2013-03-20 [已吊销:2015-01-01]
uid [ revoked] Tom Li (Biergaizi) (My Gmail) biergaizi2009@gmail.com
uid [ revoked] Tom Li (Biergaizi) admin@biergaizi.com

吊销原因:密钥已泄漏

This key was considered as a unsafe key, because the private key was copied to multiple computers, including some semi-public computers. Although there is no any evidence shows anybody stole my private key, but I think I’d better to revoke the key and generate new keys. The new keys are E88E8D6D (crypt), 01EE20CD (sign), and 26DC385B (auth). Tom Li Thu Jan 1 16:31:50 CST 2015

服务器 SSH 密钥变更

从即刻起,服务器 SSH 的密钥变更为 c5:87:39:99:9f:1f:74:c0:36:83:44:93:c8:c3:bd:d9 (ECDSA)。请从 .ssh/known_hosts 中删除 biergiazi.com (106.187.38.29) 的旧记录,并重新核对指纹并信任服务器。同时,SSH 使用的加密算法限制也更加严格,3DES、RC4 等垃圾弱算法将不被服务器接受。

望互相转告,以免影响诸位的正常使用。

什么?你说 NSA 在 ECDSA 中植入了后门?那也比继续用之前貌似早就泄漏的 RSA 密钥强。

备注:

PuTTY 受到英国的密码学出口限制,以及专利的障碍,导致虽然 ECDSA 一直在 wishlish 中,但几年后依然没有实现,无法正常连接使用 ECDSA 的服务器。而且,PuTTY 开发早已不活跃。

推荐使用以下替代品,同为 FOSS:

  • IceIV + PuTTY

    • PuTTY 的 fork,操作与界面不变,但增加大量新特性,甚至支持 ADB
  • TeraTerm

    • 早年作为 Windows 超级终端 的替代品开发,可扩展性强,甚至支持 VBA 宏,而且默认的配色方案是好评如潮的 Solarized

想加密数据?不要购买 ACOS5-64 智能卡!

如果你熟悉并使用以 RSA 为代表的非对称加密算法,来加密文件、签名电子邮件或者登录 SSH 服务器,那么你肯定会遇到密钥便携性的问题。如果你带着个装有私钥的 U 盘四处跑,私钥什么时候被拷贝走了估计你也不会知道。

为了解决这个问题,可以将私钥放到一个黑盒里,并由黑盒完成全部的加密和签名,同时没有办法从中获取私钥,还可以设置 PIN 码。这个黑盒就是智能卡。有两种规格的智能卡可以实现上述需求:其一是 OpenPGP card,由 FSFE 主导开发,开箱即用,但很难获得;第二是各类符合 PKCS#11 标准的智能卡。

ACOS5-64 就是一款 PKCS#11 的智能卡。它不但支持非对称加密,还支持对称加密,从特性上看,是一款十分强大的智能卡。然而千万不要购买它,这卡……没!有!驱!动!对,你没有看错!

既然支持 PKCS#11,那么应该直接使用通用工具就可以了吧?图样图森破!PKCS#11 允许厂商提供一个动态库来提供 PKCS#11,而这个动态库就相当于私有驱动。而 ACOS5-64 支持 PKCS#11,却没有提供驱动。事实上,ACS 公司确实开发了相关的驱动,但附在 SDK 里,需要另行购买,然而,这个驱动究竟是 Windows Only,还是也有 so 模块我们不得而知。而且,就算有 Linux 模块,但因为这是二进制驱动,因此无法在非 x86 下正常工作。

但不同于其它卡片的是,此卡的具体规格和 Datasheet 均可获得,不需要签署 NDA。曾有一 MIT 学生在 2010 年企图在 OpenSC 框架下实现此卡驱动,然而他仅仅完成了基本功能,以及 RSA 1024 的支持,最终因为众多坑放弃开发。

也就是说,这卡在任何系统平台与应用程序上,均完全无法使用!

wget 滚动显示文件名的 Bug —— 低级错误永不可避

更新 2:wget 1.16.1 版本已包括补丁,问题彻底解决。
更新 1:我的补丁已经被接受了,Bug 已修复。

wget 是 GNU 开发的实用下载工具,最近它刚刚发布了 v1.16。

进度条样式

先介绍一下背景,wget 向来就有两种进度条,“点形”和“条形”,其中,“条形”还有可选的跑马灯效果。

刷屏点形”是指这样的效果

  0K .......... .......... .......... .......... ..........  0%  107K 8m30s
 50K .......... .......... .......... .......... ..........  0% 52.0K 13m0s
100K .......... .......... .......... .......... ..........  0% 31.3K 18m21s
150K .......... .......... .......... .......... ..........  0% 22.0K 24m4s

如果你的终端设备功能有限,不能做到即时更新屏幕内容,或者是重定向到了文件,“点形”进度条就很实用。

“条形”就是指这样的效果

filename   0%[                     ] 134.05K  59.7KB/s

很适合可以即时更新的终端。

这两种类型可以使用

wget --progress=dot
wget --progress=bar

切换。

跑马灯

现在问题来了。如果文件名称很长,条形进度条左侧那一点点空间显示不下该怎么办呢?wget 的开发者想出了一个跑马灯效果,很像大街上的 LED 横幅,不停的让文字滚动,这样用户就可以“管窥”整个文件名了。

然而,盖子发现一个特别郁闷,而且能逼死强迫症患者的问题。wget 的滚动有 Bug,文件名的最后一个字符始终不显示!就象这样:

this_is_a_
his_is_a_f
is_is_a_fi
s_is_a_fil
_is_a_file
is_a_file_
s_a_file_n
_a_file_na
a_file_nam
            <-- WTF!
this_is_a_

实现

progress.c 中,不难发现这段代码

  if (((orig_filename_cols > MAX_FILENAME_COLS) && !opt.noscroll) && !done)
    offset_cols = ((int) bp->tick) % (orig_filename_cols - MAX_FILENAME_COLS);      
  else
    offset_cols = 0;
  offset_bytes = cols_to_bytes (bp->f_download, offset_cols, cols_ret);
  bytes_in_filename = cols_to_bytes (bp->f_download + offset_bytes, MAX_FILENAME_COLS, cols_ret);
  memcpy (p, bp->f_download + offset_bytes, bytes_in_filename);
  p += bytes_in_filename;

由于有一些字符占用 1 字节,有些占用 2 字节,因此下面部分的代码全都在处理把字符转换成字节数的问题,其实这段代码做的事情很简单

 if (orig_filename_cols > MAX_FILENAME_COLS
     && !opt.noscroll  // 没有禁用跑马灯滚动效果
     && !done)  // 下载仍未结束

     offset_cols = bp->tick % (orig_filename_cols - MAX_FILENAME_COLS);

bp->tickint,每刷新一次进度条,它会就自增 1,可以把它理解成进度条刷新的次数,(orig_filename_cols - MAX_FILENAME_COLS) 就不用多说了,显然是计算文件名超出最大允许长度的字符数。

最后,从 offset_cols 开始截取 orig_filename_cols,一直截取 MAX_FILENAME_COLS 个字符。

用取余数运算来实现不断截取字符串的特性,看起来还是挺巧妙的。不过,正是这里的代码存在着问题。

范围差 1

写程序的时候,经常因为该 + 1/- 1 而忘记了(len() vs. 下标),不该加减 1 的时候乱加减,导致典型的越界往往和正确范围只差 1 位。

再比如这些令人困惑的表述

def range(begin, end)
/* 11 日到 21 日间断电 */
/* 变量取值范围 0 ~ 256 */

到底包不包括这个 end,或者包不包括 21 日,或者 256?P.S:幸好数学家们早就意识到了这个问题,发明了区间表示法,圆括号表示“排除”,方括号表示“包括”。

wget 的“最后一个字符始终不显示”的 Bug,具有典型的“范围差 1”的特征,那真正的问题到底出不出在这里呢?

实验

为了看看 wget 的这个算法到底有没有 Bug,写个程序检验一下。

FILENAME = "this_is_a_file_name"
MAX = 10

def cut(string, _min, _max):
    assert _max <= len(string) - 1 
    return string[_min:_max]

for i in range(0, 20):
    offset = i % (len(FILENAME) - MAX)
    print(offset, offset + MAX, cut(FILENAME, offset, offset + MAX))

这就是 Python 版本的简单算法实现了,运行之后:

0 10 this_is_a_
1 11 his_is_a_f
2 12 is_is_a_fi
3 13 s_is_a_fil
4 14 _is_a_file
5 15 is_a_file_
6 16 s_a_file_n
7 17 _a_file_na
8 18 a_file_nam
                 <-- WTF!
0 10 this_is_a_

果然出错?那么问题究竟出在哪里呢?拿这个例子分析一下,”this_is_a_file_name” 有 19 个字符,而最大的允许字符是 10 个。那么,那么,相差 9,看来编写者认为 9 就是需要的滚动次数。然而,滚动 9 次,就是有 10 种组合啊!

果然忘记 + 1,而接下来就不用多说了。就等开发者接受补丁了。真是低级错误不可避啊……

OpenRC 无法正确配置 IPv6

Gentoo 的 OpenRC 配置 IP 的方式是非常简洁的,只需简单修改 /etc/conf.d/net 就能完成任务。
然而,比尔盖子的 Gentoo Hardened 一直无法正确的配置 IPv6,主要表现是可以配置地址和 DNS,却认为网关是无效地址。

config_eth0="106.187.49.164/24
             2400:8900::f03c:91ff:fe73:f8c7/64
"
routes_eth0="
    default via 106.187.49.1
    default via fe80::1
"

dns_servers_eth0="106.187.34.20 106.187.35.20 106.187.36.20 2400:8900::2 2400:8900::3"

有问题的就是这个 fe80::1

今天解决这个问题,发现这个问题的成因和解决方案都简单的气人。事实上,OpenRC 支持使用不同的工具初始化网络。在默认情况下,OpenRC 初始化网络的工具是最传统的工具,这些工具对 IPv6 支持有问题。而新的 iproute2 就没有这个问题。

emerge iproute2

然后在 /etc/conf.d/net 行首加 modules="iproute2" 让 OpenRC 使用 iproute2 的工具链配置网络即可。

高性能 ext4 临时文件系统

很多时候,我们的文件系统中并不保存重要数据。比如 /var/tmp, /usr/src/usr/portage,对于这些目录,我们可以专门创建一个文件系统,并以牺牲数据安全性(因为我们不需要)换取最高性能。

如果你的 ext4 分区中储存的是 /usr/portage 这样大量小文件构成的数据库,可以使用以下参数格式化:

mkfs.ext4 -b 2048 -i 2048 -O "dir_index" /dev/sdX

-b 2048 -i 2048 确保分区中有足够的 inodes,避免因大量小文件导致明明有空间却没有 inode 的困境发生;”dir_index” 会让内核以类似 ReiserFS 的 btree 储存文件,对于大量小文件有很大的性能提升。但其它类型的数据分区,比如 /usr/src/var/tmp 请勿使用此方法格式化,请使用默认参数。

使用不安全的写入方式提升性能:

tune2fs -o journal_data_writeback /dev/sdX

禁用日志:

tune2fs -O ^has_journal /dev/sdX

使用以下挂载参数(写在 /etc/fstab):

defaults,noatime,nodiratime,barrier=0,nosuid,nodev,data=writeback

noatimenodiratime 彻底禁用一切访问时间记录;barrier=0 禁用为安全保护而设计的写入屏障;nosuidnodev 提升安全性;data=writeback 使用不安全而最高效的写入方式。

效果拔群!

服务器 SSH 端口再次变更

由于众所周知的原因,现将服务器端口号重新变更为 22000。望互相转告,以免影响诸位的正常使用。

啥?你说我为啥不用 iptables 转发端口?抵抗的行为会让服务器 IP 遭殃的……

GRUB 2 里的 play 命令

GRUB 2 中,有个 play 命令,用来让蜂鸣器播放音频。这可不是什么玩法……

它接受无限个参数,对于一个典型的三参数的调用,文档如下

play [tempo] [pitch] [duration] {pitch2 duration2...}

tempo 是全局速度,所有的 pitch 都受到它的影响;pitch 即要播放的音频,单位为赫兹;duration 是持续时间。

为了播放音乐,需要搞清楚一个调用究竟是多长时间。把 play 看作一个函数,接受 tempo 和 duration 两个参数。经过试验:

tempo duration 时间
60 1 1 s
60 2 2 s
30 1 2 s
30 2 4 s
120 1 1/2 s
120 2 1 s

从这里大概可以看出来,

时长 (s) = 60 / t * d

这样,如果使 t = 60000,d 就正好对应于 0.001 s,即一毫秒。这样,就可以轻松的将基于毫秒时长的 Linux 下 beep 调用翻译成 GRUB 2 的 play 调用了。

另外,这还可以用于 sleep。GRUB 2 提供的 sleep 分辨率只有一秒,但使用 play,让 pitch = 0 Hz,就曲线实现了毫秒级的 sleep 命令。

服务器君用了 1.469 秒钟,查询了 39 次数据库(17 次被缓存),消耗了 11.02MB 内存,努力为您呈现了这个页面。