博客迁出 yo2.cn

近两年以来我都是使用的 yo2.cn 提供的博客托管服务,就是说 yo2.cn 提供一个 WordPress 空间,我将域名 blog.solrex.cn 绑定到这个空间上。对 yo2.cn 的服务,总的来说,我还是基本满意的,不然我也不会一年后续费继续使用。但是一直以来的服务器不稳定,尤其是最近两个月来的宕机事件,让我对 yo2.cn 的服务失去了信心。09 年 3 月 14 日到 17 日,09 年 5 月 12 日到 15 日,两个月里两次长达三天的宕机事件在我的 Google Analytics 中划出了两个漂亮的谷底!我只好说,拜拜了 yo2。

以前之所以选择 yo2.cn 的服务,主要原因是(1)懒得折腾 WP 系统(2)它的服务器在国内,教育网用户可以无障碍访问(至少我女朋友在使用 CERNET)。这次将博客迁移到国外主机上,可能会造成部分教育网用户无法访问,请见谅。如果您希望继续关注此博客,请使用 Google Reader抓虾等在线阅读器订阅此博客的 RSS

现在博客的系统和主题和原来保持一致,域名也没有变化,所以对一般读者来说,访问没有任何影响。在某些网络状况下,可能速度会比原来慢一点儿;DNS 没有刷新的网络,可能要等待blog.solrex.cn域名记录的刷新;在一些存在缓存的浏览器中,可能要重刷一下缓存。

对于我来说,却是方便了许多,得到了对 WordPress 系统的完全控制,也没有了关键字的过滤,使用起来就没那么窝心了。

用 Vim 对矩阵转置

前两天某个同学在科苑星空 BBS 上问到了一个有趣的问题:如何在 Vim 中对矩阵进行转置?

我当时想,转置不就是行列互换嘛,awk 可以取一列,那么拿出来每列然后打印成一行不就好了?类似于:

echo `awk '{printf "%s ",$1}' file`
echo `awk '{printf "%s ",$2}' file`
echo `awk '{printf "%s ",$3}' file`
...

本来觉得用 bash 写一个循环语句就可以了,但是怎么也尝试不出来如何替换那个 $1, $2, $3...就想没办法了只能搞 eval 了。但是我觉得这种事情 Unix 前辈们应该干过,所以就搜了一斧子,果然搜到了一个 AWK 程序,《Sed & Awk》 Ch13.9 Perform a Matrix Transposition:

#! /bin/sh
# Transpose a matrix: assumes all lines have same number of fields

exec awk '
NR == 1 {
    n = NF
    for (i = 1; i <= NF; i++)
        row[i] = $i
    next
}
{
    if (NF > n)
        n = NF
    for (i = 1; i <= NF; i++)
        row[i] = row[i] " " $i
}
END {
    for (i = 1; i <= n; i++)
        print row[i]
}' ${1+"$@"}

哈,除了觉得这样内存占用可能比较大之外,这可是一个相当不错的程序。

在 Vim 中调用就很简单了,将上面脚本保存成 transpose,加上可执行属性放在某个可执行路径下(比如 ~/bin),然后在 Vim 编辑矩阵文件时 :%!transpose 就可以了。

PS: 另外,在查证矩阵的“转置”应该写成“转秩”还是“转置”时,我在 Wikipedia 发现一个很有意思的东西:

矩阵用词
在中国大陆,横向称为“行”,纵向称为“列”。 在台湾,横向称为“列”,纵向称为“行”。

天那,要是用汉语和台湾同学讨论矩阵的话,该有多痛苦呀!

Vim 中文输入法插件 Vimim

有位朋友在 TopLanguage 讨论组里向我推荐了一款非常 cool 的 vim 输入法插件:vimim,您可以从 Vim 插件页下载 Vimim,演示程序和码表可以在 Vimim Googlepage下载。Vimim 作为一个 Vim 插件项目起始于 09 年一月份,可能知晓它的用户还比较少,所以我觉得有必要向大家推荐一下这款优秀的 Vim 插件。

作为中文用户,在 Vim 里输入中文往往是一件非常痛苦的事情,最别扭的地方在于使用中文输入法时进行 Vim 编辑模式之间切换比较麻烦,还容易误操作,这也是我一般不使用 Vim 编辑中文文档的原因。

简单地来说,这个插件使得你在 Vim 里直接输入中文(使用 Ctrl+\ 开启选词单而不是通常输入法使用的空格键)。当你不按 Ctrl+\ 时,你就是在输入英文字母,这样就避免了中英文输入法切换的问题。这个插件的使用方法和演示在 Demo 页展示得很清楚,这里我就不再赘述了。

不过在使用上我仍然面临着一个小问题,Vimim 目前提供的双拼码表是基于微软双拼的,而我习惯于智能 ABC 双拼,所以我在 Vimim 的讨论组里抱怨了一下:《希望双拼音节映射独立于码表》。

------------------------------ 我是分割线 ------------------------------

我的 BSP yo2.cn 服务器宕机从 09 年 5 月 12 日起延续了将近三天,期间可能导致部分用户遇到无法访问问题,请见谅。

多余的逗号?

晚上看了两页 The Art of Unix Programming,其中提到了一个我以前一直感觉困惑的地方:

在我看过的 C/C++ 语言程序代码中,为什么有的列表初始化时在最后元素后会加逗号“,”,而有的不会?
例如:int[] a = { 1, 2, 3, };

书中的原话倒不是讨论逗号该不该加,而是说到了这样做能带来的好处:

A good example is C accommodating an extra comma at the end of an array initializer list, which makes both editing and machine generation of array initializers much easier.
-- The Art of Unix Programming (TAOUP) Ch8.3.1

哦,虽然我一直体会到这样做的好处(尤其当列表成员又臭又长且要经常修改时),也晓得这样做不会引起编译错误,但我经常是在代码 stable 之后将最后的逗号去掉——原因无它,不确定这样做是不是没有问题,那么还是尽量避免吧。今天忽然看到 TAOUP 提到这个,我就好奇:到底是 C/C++ 标准允许这样做呢?还是编译器的实现大部分支持这样做?于是就查了一下。

结果让我很开心,C/C++ 标准中就允许这样做:

initializer:
    assignment-expression
    { initializer-list }
    { initializer-list , }

-- ISO/IEC 9899:1999 (C99) Ch6.7.8 §1

initializer-clause:
    assignment-expression
    { initializer-list ,opt }
    { }

-- ISO/IEC 14882:1998 (C++98) Ch8.5 §1

K&R 中也用非常简短的一句话提到了这个特性:

A list may end with a comma, a nicety for neat formatting.
-- The C Programming Language (K&R) Appendix 8.7

这意味着(C/C++ 语言中)在元素列表最后加上一个逗号是一件非常安全的事情,看来我以后不必再考虑删除列表最后那个逗号了,这样能省却我很多麻烦。

延伸阅读:在其它编程语言中,是否支持这样做呢?Arrays: additionnal commas 这篇文章进行了一个很有意思的讨论。

NetworkManager 任重而道远

Ubuntu 8.04 以前的 NetworkManager(nm-applet) 还差强人意,好歹还能通过它来配置一下网络。中间跳过了 Ubuntu 8.10,而新的 Ubuntu 9.04 的 NetworkManger 则让我不知道该如何使用。

我依稀记得原来使用 nm-applet 对网络进行配置后,设置会被保存在它应该在的配置文件中,比如 /etc/network/interfaces, /etc/resolv.conf。但是我完全不知道目前的 nm-applet 会将网络配置保存在什么地方。无论是有线网络还是无线网络,无论是 DHCP 还是固定 IP,我一次都没有使用 nm-applet 配置成功过。有线网络还好,我很熟悉,直接写到配置文件中不费什么事情。最近忽然遇到要配置无线网络,一懒,就试试 nm-applet。用 nm-applet 可以扫描到无线网络,但是填入密钥就是无法完成验证,没办法只好转而用 iwconfig 命令配置。按说直接敲命令应该是最靠谱的事情,依然无法凑效,最后才发现原因——在使用 iwconfig 配置时,一定要 disable nm-applet 对无线网络的管理!

我觉得目前 nm-applet 可能在做一些革新性的改变,大概是想简化网络的配置方法增强易用性吧。但是一些不愉快的经历让我觉得现在的 nm-applet 是一个相当不靠谱的东西,所以我只好在所有的 Ubuntu 里禁用了 nm-applet,还是直接修改配置文件靠谱呀!

谈谈 CoolSIP

说明:本文有一些内容是基于本人的合理猜测。如果您知道更多细节,请不吝指出,但最好请提供事实依据,而不是仅仅指责我主观臆测,不尊重事实。我不知道事实,才会猜测,我如果知道事实,就不用猜测了。

CoolSIP 是清华大学信息网络工程研究中心建设的 IPV6“下一代互联网应用示范平台”提供的一个在 IPV6 网络上运行的 VoIP 软件,该软件提供了主机间通话和主机拨打传统 PSTN 电话的功能。目前该服务仅提供给 IPV6 教育和科研网用户使用,注册该服务需要 edu.cn 或者 ac.cn 后缀的大学或科研机构电子信箱。测试期间是免费的,但需要用在线时长换取话费。

我这里呢,就是说几句闲话,谈谈我对这个软件和服务的看法。

1. CoolSIP 的知识产权问题

我对 CoolSIP 软件的结构并不清楚,但基本可以肯定的一点是 CoolSIP 使用了开源的 osipeXosip。不用逆向工程该软件,查看 CoolSIP 帐户信息的保存位置就可以知道,CoolSIP 的帐户信息保存在 C:Documents and SettingsUsernameeXosip 目录下。

由于 osip 遵从的是 LGPL 协议,eXosip 遵从的却是 GPL 协议,那么从我有限的对知识产权的了解(关于 LGPL 和 GPL 可以参考这里),CoolSIP 也应该遵从 GPL 协议(对这点有问题您可以在评论中指出)。而目前来看,清华大学信息网络工程研究中心并没有发布 CoolSIP 的源代码,而且 CoolSIP 貌似将 osip 和 eXosip 库静态链接进了程序里(大概有隐藏的意图),那么可以认为他们并不认可 GPL 许可证——不过他们还没有错误地在安装文件中包含一份私有版权声明。

另外,http://ngmylife.wirelesslan.edu.cn/multimedia.html 页面中说:“当eTone出现无法登录的问题时,可以使用CoolSIP试试。”可以猜测 eTone 和 CoolSIP 有非常紧密的关系,而 eTone 又是一款流行 VoIP 软件“快门” 的前身,那么我对“快门”软件的版权也持谨慎地怀疑态度——不过我更倾向于快门不大可能犯这种错误。

2. CoolSIP 本身的程序设计问题

CoolSIP 是一个极不成熟的软件,使用过程中居然会出现程序错误,点“忽略”才能继续运行的情况;由于其版本历史仅仅显示到 2007 年 9 月 3 日 ,我想其对 SIP 协议的实现是存在问题的,例如在拨打一个通话中的电话号码时,CoolSIP 不会提示对方正在通话中。总之,该软件除了是 sip6.edu.cn 提供的 VoIP 服务的官方客户端以外,几乎一无是处。

3. CoolSIP 的 VoIP 服务

虽然 CoolSIP 客户端没有什么优点,但是 sip6.edu.cn 的 VoIP 服务器端对 SIP 协议的实现应该还算很标准的。由于 CoolSIP 使用了 eXosip 库,那么我就尝试使用另一款使用 eXosip 库的遵从 GPL 协议的开源软件 Linphone 尝试连接了一下 CoolSIP 服务器,居然能正常连接。

并且 Linphone 显示出比 CoolSIP 更友好的用户体验,例如:Linphone 支持 Windows 和 Linux 系统,更完整地支持 SIP 协议,支持自定义电话联系人列表,多帐户同时在线,通话统计记录显示,多种音频编码,更好的视频支持。像上面提到的 CoolSIP 不支持的“您拨打的电话正在通话中”提示,也可以在 Linphone 中听到。

官方提供的专门客户端 CoolSIP 竟然不如一个开源的通用客户端 Linphone,这大概是“下一代互联网应用示范平台”服务的一个笑柄吧。

PS: 使用 Linphone 登录 CoolSIP 的服务器,需要 libexosip2 和 libosip2 版本大于 3.3,我的经验表明 3.1 会出现较多问题。推荐手动依次编译 libosip2->libeXosip2->linphone。Windows 版的可执行安装程序包含了这两个库,所以不存在库版本低的问题。

请测试 JabRef 2.4.3 中文版

JabRef 2.4.3(未发布)对中文用户的一大改进就是开始支持中文界面。中文化的工作本来是由另外一个朋友和我共同来做,但由于他工作繁忙,后来就是我一个人在进行了。

目前我已经翻译了大约 80% 的样子,菜单和选项窗口基本上都翻译完了,还剩下一些少用的选项和状态栏信息。因为水平有限,也并不能遍用 JabRef 的所有 feature,很多地方可能翻译的不合适,所以想找大家帮忙测试一下,给提些意见。(JabRef 使用问题请参考“文献管理软件 JabRef 快速入门”)

您可以在 http://share.solrex.org/ibuild/ 下载到编译好的 jar 文件,jar 文件以 JabRef-2.4.3-Chinese-编译日期.jar 格式命名。您可以使用

java -jar JabRef-2.4.3-Chinese-编译日期.jar

来执行它。

如果您已经安装 JabRef 2.4.2 版,您可以覆盖 JabRef 2.4.2 的 jar 来实现直接从原有快捷方式启动 JabRef 2.4.3,方法为:

Windows:

首先到您的 JabRef 2.4.2 安装目录,我们假设为:C:Program FilesJabRef,备份 JabRef-2.4.2.jar(修改文件名为另外的名字即可),然后将 JabRef-2.4.3-Chinese-编译日期.jar 拷贝到安装目录 C:Program FilesJabRef 下,修改文件名为 JabRef-2.4.2.jar,然后再执行 JabRef.exe 就调用的是 2.4.3 版本的 JabRef 了。

Linux:

sudo mv /usr/share/java/JabRef-2.4.2.jar /usr/share/java/JabRef-2.4.2.jar.bak
sudo cp JabRef-2.4.3-Chinese-编译日期.jar /usr/share/java/JabRef-2.4.2.jar
当然,您也可以修改软链接 /usr/share/java/jabref.jar 指向 JabRef-2.4.3-Chinese-编译日期.jar

启动 JabRef 2.4.3 之后,请到 Options->Preferences->General->Language 中选择 Chinese,然后重启 JabRef 才能进入中文界面。目前只有简体中文,但是我已经提请维护者增加并区分简体中文和繁体中文,大概会在以后添加。

您可以通过以下途径将有关翻译的意见反馈给我:
1. 评论这篇文章,任何人。
2. 在我侧栏的 Google Friend Connect 留言簿留言,需要 Google 或 OpenID 帐户。
3. 给我发送 Email,我的联系方式可以在导航栏的“关于我”链接中找到。

目前所有翻译都是我的工作,所以所有错误都该归我一个人身上。有您的帮助,我才能不将这个错误带给更多人,我将很感激您为此付出的时间和努力。

下面是中文版的一个截图:
JabRef 中文版界面

Dell MOC5UO Mouse Sucks

在使用的第一天我就发现了,Dell 这一款 MOC5UO 鼠标会让我的小指很难受,现在我更加确信这一点,它已经成功地使我的小指成为每天疲劳感的源泉。

不知道是哪个糟糕设计师的设计,居然把鼠标做成杠铃的样子,中间窄两边宽。在使用时拇指还好,小指要么收缩很多才能抓住鼠标,要么翘起来只用其余四根指头抓鼠标,时间一长就会造成小指负重过多,整个指头难受的不行。手掌越是宽大的疲劳感越明显。

昨天我比较了一下 Dell MOC5UO 与中关村卖的那种盗版 IBM 小鼠的宽度,发现 MOC5UO 最窄的地方比 IBM 小鼠还窄。妈呀,小鼠标已经很难用了,Dell 你出一个大鼠标居然比小鼠标还窄!最恶心的是 Optiplex 740 还不带 PS/2 插孔,我想换以前的 PS/2 机械鼠还不能换,fxxk! 这就是传说中的“蜂腰式”造型,人体工学设计?他们绝对没有让正常手型的男人试用过这款鼠标。我真怀念那些没有“人体工学设计“的鼠标!

DELL MOC5UO 鼠标

首钢轮滑

时代在变迁呀,小时候我们把滑旱冰的鞋叫做“旱冰鞋”,现在我们把旱冰鞋叫做“轮滑鞋”;小时候我们说“溜旱冰”,现在我们说“刷刷”——只有 Baosheng 这种“圡人”还说:“玩了一晚上旱冰”,哈哈!

自从去年冬天跟轮滑社去鸟巢“刷刷”以后,我就再也没穿过轮滑鞋。所以当跟我一起开始玩轮滑的兄弟已经成为所谓“高手”开始收徒时,本人还停留在“菜鸟”级别上。今儿轮滑社组织去已经搬迁的首钢大院玩,我一时兴起,也跟着一起去了。

玩嘛也无非是在大院里穿着鞋到处转,乏善可陈。不过我干了一件非常莽撞的事,从一个长坡上直接冲下来,结果在半中央失去控制摔到路边上了。幸亏戴着护具,没有什么身体伤害。特以下图纪念我的愚蠢行为,以后这种坡我再也不往下冲了:

如果您看不到这张照片,请点击到 Google 相册查看。

Google 音乐搜索

/* 这不是一篇商业软文——虽然我希望可以有钱赚 :) */

Google 音乐搜索

今天无意中溜到和菜头的博客,看到他对谷歌音乐搜索的评论。然后试了一把,您还别说,真不错。

虽然这个音乐搜索并不像一个搜索网站而更像一个常见的音乐站,但是界面足够清爽,而且链接不会失效。我平时访问某些音乐站时最讨厌的就是页面上一堆链接,而且很多音乐是从别的网站盗链过来,经常失效。

最赞的是那个音乐播放器,列表功能很强大,而且还可以同步显示歌词。而且经过测试发现网速足够快,一首歌缓冲个两三秒就下载完了,完全可以在线听。我觉得以后没有必要在电脑中保存音乐了,这样一来,使用 Linux 播放 mp3 的 ID3 标签问题也解决了,而且 Linux 下也一直没有找到可以同步显示歌词的软件。

但是一个问题是播放列表无法保存,为什么不能把播放列表和 Google 帐户联系起来呢?

惊艳 rin-wendy.com

这个网站的 WordPress 主题太——漂亮了!!!太可爱了!!!太喜欢了!!!

http://rin-wendy.com/

别找了,作者没有释出主题下载。国内论坛上曾经有人用蜘蛛抠下来了主题文件,但是碰巧 Rin 其实也是华人,人家能看懂中文。在 Rin 的强烈反对下都被删贴了。

我好喜欢这样的主题呀!不知道哪里有类似的释出的主题下载?我拿来自己改改也成呀!

关于 eYouIPB 和 CASNET

我不知道为什么在干正事的时候想写这个东西,大概也算是一种强迫症吧,想起来某件事情就总觉得有义务马上去做。那么就赶快写完吧。

这篇文章是写给有心人看的,如果您不知道 eYouIPB 和 CASNET 是什么东西,那么您可以无视以下内容。

eYouIPB 是中科院研究生院目前使用的网关登录客户端,但只能在 Windows 下使用。我用 Python 写的一个小软件 CASNET,就是一个 Linux 下的登录替代品,不过目前它也可以支持 Windows。昨天有同学发信问我有关科苑网关登录的问题,我顺便在这里做个笔记吧。

1. 有关流量统计。目前 CASNET 不支持像 eYouIPB 那样的实时的流量统计功能,因为我觉得没有必要。科苑上网是按照流量收费的,而这个流量是指网关服务器统计的流量,而不是客户端统计的流量,所以客户端的实时精确流量统计没有太大意义。

eYouIPB 带有实时的统计流量功能,但这却造成了一些问题。eYouIPB 使用 WinPcap 库做流量统计,而且这个库的版本比较低。假如系统里安装了新的 WinPcap 库,例如 Wireshark(Ethereal) 网络监听程序就会安装 WinPcap 库,那么 eYouIPB 就无法工作了。WinPcap 是一个非常强大的抓包库,而 eYouIPB 仅仅使用了其中的流量统计功能,实在很让人费解它为什么要使用 WinPcap 库并且将它的 dll 包含在自己的软件中。我想应该有更简单的办法做流量统计——如果非做不可的话。

2. 有关登录方式。科苑上网有两种登录方式:一种是 eYouIPB,使用私有的协议;一种是网页登录,使用 https 表单交互。由于 eYouIPB 协议私有,CASNET 只能模拟网页登录的方式,所以您会发现 CASNET 登录速度会比 eYouIPB 慢一点儿,因为它有一个完整的 https 安全协商和交互过程,而 eYouIPB 只需要几次简单 tcp 数据包交换就可以了。不过因为同在一个局域网内,这个速度还是在可以忍受的范围之内。

3. 有关安全性。去年我曾经研究过 eYouIPB 的协议,发现还是相对简单的。我记录的笔记已经丢失,但大概是这个样子的(应用层):“首先 eYouIPB 向服务器发起一个连接,服务器应答可以连接,然后 eYouIPB 将用户名和域发送给服务器,服务器看上去会返回一个密钥,然后 eYouIPB 使用该密钥用某种加密算法加密用户口令(或者还有其它信息)发送给服务器,服务器验证口令,回应是否可以登入。”

从该协议的实现来看,安全性有限。一个是用户可以通过不停地登入登出收集明密文对,另外反汇编 eYouIPB 看起来也不是那么困难,尤其是可以使用 Winsock 32 的 API 来定位负责加密的代码段。一旦加密算法被了解,窃听 eYouIPB 的数据包就能获得用户的口令,那么帐户就可以被窃取。所以相比而言,https 的登录方式更值得信任。

PS: 今天顺便搜索了一下,有个朋友曾经对 1.03 版本的 eYouIPB 协议进行过分析,我感觉和 2.0 版本差距不大。而且这个人完全用 C 实现了 eYouIPB 1.03 的加解密函数如果他仅仅凭逆向工程做出这个结果的话,我是相当地佩服的(因为我花了一整天的工夫也没有做出来一点儿东西)。如果当初我能看到这个结果的话,说不定就有 clue 去模拟 eYouIPB 的协议而不是使用 https 方式了,现在是懒得再去钻研这个东西了。只是不知道 2.0 是否还在使用同样的加解密函数,如果仍在使用的话,那么对有心人来说这个口令保护措施基本上算是不存在了。

4. 有关软件运行速度。这个比较主要是 Windows 平台下,CASNET 是用 Python 脚本写成的,而看起来 eYouIPB 使用 VC++ 写的,在软件的大小和运行速度上 eYouIPB 要显然优于 CASNET。由于 CASNET 的 Windows GUI 版需要额外的 GTK 运行时库和 Python 库支持,大概会加载到内存中 16M 左右的数据,不过这些东西运行时真正用到的不多,所以有些部分除了初始化时,其它时候很可能是驻留在交换区中,所以还是可以接受的。

在 Linux 平台下,因为 GTK 和 Python 库是被非常广泛使用的,所以 CASNET 并不需要额外占用很多内存。

之所以会发布 Windows 版,是因为相比 eYouIPB 而言,Wireshark 对我更重要,所以我无法使用 eYouIPB,而又讨厌网页登录时每次都要选择下拉菜单。再加上顺便看看 PyGtk 在 Windows 下的表现如何。但比较讽刺的是,Windows GUI 版的 CASNET 下载量要超过其它版本加一起的下载量,看来它的存在还是对某些用户有一些帮助。

5. 有关软件功能。eYouIPB 的功能已经被锁定了很多年,但是 CASNET 一直在根据用户的需求增加或者删减某些功能,例如余额不足提醒,断线自动重连,一键切换登录模式等。还有一个未发布的功能是关机自动下线。这是一个我一直想实现的功能,因为当用户关机忘记离线时,恰好分配到原 IP 的用户就可以使用该帐号,造成流量被窃取。这个功能在 Linux 版本上已经基本实现,但是 Windows 版本仍然没有找到可用的方法实现。

春游戒台寺

我今天第一次骑了山路,是真正的盘山公路耶!继昨天骑车逛了卢沟桥之后,心里痒痒,就想再走远一点儿吧,于是就有了戒台寺之行。

本来还想着如果还有气力的话,继续往前骑到潭柘寺。谁知道从石门营环岛开始的纯上坡盘山公路把我搞崩溃掉了。前三公里我还是勉强骑上去的,后三公里实在受不了了,半走半骑,再加上路边歇歇,六公里长的路我花了一个小时才上去。

在戒台寺下面路口还碰到了东方红论坛的几个车友,见了我直夸我车“好”,还给我照了张相。天那,我以后再也不骑不带变速的自行车上山路了。我哪里知道到戒台寺有那么长的山路呀,还是纯上坡!

到戒台寺上面的平台后,遇到了在路上碰到的一个朋友,我们俩就坐在寺门口的石台上闲扯了两个小时。戒台寺的位置真不错,从门口的平台上居然能看到中央电视塔!让我深刻理解了“望山跑死马”这句话。然后他继续往潭柘寺,潭柘寺还有九公里,我琢磨着我得爬着才能上去,算了,还是打道回府吧。

下山的路好爽,我只顾着捏闸了,六公里下坡,十五分钟滑到底。那个痛快呀,觉得上山的时候受的苦也值了!

路线是从玉泉路往南,到莲石东路辅路右转向西直行,到京原路左转,沿京原路到卢井路交叉口,偏右直行上国道 G108,沿 G108 到石门营环岛左转 G108 上山,总共大约 20 公里。去的时候由于走错路(京原路实在太绕了)加爬山,一共花了两个半小时;回来时候按原路返回,正好一个小时到学校。

如果您看不到这张照片,请点击到 Google 相册查看。

骑行卢沟桥

昨天我把签名改成“明天骑车去卢沟桥吧”后,部分同学有点儿惊讶。我以前也以为卢沟桥好远,但当我用百度地图丈量了一下到它的距离之后,我有了充足的信心。猜猜卢沟桥到玉泉路的距离是多少?不到 10 公里,比中关村到玉泉路近多了。

另外,我也不是什么车协的驴友,虽然我曾有过买一辆好车的良好意愿,但玉泉路校区严格的宿舍管理制度使我放弃了这个想法。谁愿意把一辆上千人民币的车停在露天的楼下呀?所以我现在只有一辆永久买菜车可供差遣。

路程其实很顺的,从玉泉路地铁站往南直走,到小屯路南端上京石高速的辅路右拐往西南走。其实后来我发现自己犯了一个愉快的错误:本来在京石高速的辅路上看到“抗日战争纪念馆”的标识牌往右拐就可以到宛平城的东门,穿过宛平城就是卢沟桥的东门。但是我没有从那里拐,而是沿着京石高速过了永定河,到长辛店才从桥西街折回到卢沟桥的西门。不是我故意这样做,我是按照百度地图的驾车路线走的。为什么说是一个愉快的错误呢?因为我从卢沟桥西门进去时没有人查票,当时我还以为今天卢沟桥免费开放呢,后来从东门出来时候才发现,哦,还是有人买票的呀...

以前听说卢沟桥的狮子很有名,还有点儿不信,看了以后才觉得,的确是名不虚传。虽然很多石狮子已经被天灾人祸剥离了不少样子,但那些惟妙惟肖的姿态,却是在其它地方不曾见的。

东门出来就是宛平城,也是文物保护单位,里面有很多旧式的房子和店铺。大概是非旺季的原因,好多店铺都没有开。穿过宛平城时才发现抗日战争纪念馆,汗,我根本不知道抗日战争纪念馆在这里。门票是凭证免费发放的,于是进去又接受了一番爱国主义教育。

从纪念馆出来,在街上吃了碗面,就骑车回来了。本来不想沿原路返回,但走了一阵发现到处在修路,七拐八拐还是拐到了小屯路上,看来相比而言这条路还是比较顺的。

最后奉上玉照一张 :)

如果您看不到这张照片,请点击到 Google 相册查看。

FBI 终端登录主题

FBI Terminal KDM&GDM Greeter Theme

慵懒阳光下的北京虽然还有些乍暖还寒,随着穗穗掉落的杨花,春天还是到了。在这样美好又带有一点儿遗憾的春日里,我这样的宅男依然龟缩在带有落地窗的宿舍,凝望着楼下长长的北京正负电子对撞机,考虑我被辐射挂掉的可能性有多大。

下面是这样对生命深刻地思考之余的一个副产品:FBI 终端登录主题。某日看到了 Windows 下有一个很酷的 FBI Terminal 登录主题,在 gnome 和 kde look 搜索一下未果后,就决定自己做一个,产生了这个东西。谁知道在我费劲地搞出来它之后,才搜索到有类似的 Gnome 主题,不过我可以自我安慰的是它不能很好地支持我的电脑分辨率。

这是一款 Linux 下 KDE4 和 GNOME 桌面的登录欢迎主题,安装并选择它作为登录主题后,欢迎界面就会变成类似于 FBI Terminal 的样子,如图所示。其实我并不知道真正的 FBI Terminal 是什么样子,只是按照 Windows 下某款 FBI Terminal 主题进行的修改。不过依然很酷,是吧?

这款主题可以支持 KDM4 和 GDM,没有在 KDM3.x 进行过测试。目前这个主题可以支持三种分辨率:1024x768, 1208x800, 1280x1024,使用其它分辨率的用户可以按照宽高比选择相近的比率主题。

FBI 终端登录主题 for Linux 的下载页面是:http://share.solrex.org/misc/fbi_theme

另外,Happy April Fool's Day! :)

Kubuntu 9.04 KDE4 问题

这些问题也未必是 Kubuntu 的问题,很有可能是 KDE4 本身的问题。

1. .gtkrc-2.0-kde4 实际不能用。

GTK 的程序在 KDE4 下显示太难看了,尤其是菜单那块,整整突出来一个方块。Firefox 和 Thunderbird 还好,可以更换个主题啥的。像 Pidgin, scim 之类,那个难看呀,和 KDE4 的主题一点儿都不搭。

都说在 System Settings->Appearance->GTK Styles and Fonts 中选择 Use my KDE style in GTK applications 就可以了(需要先安装 gtk-qt-engine-kde4),但我发现一个很奇怪的问题。选择了该选项之后,是会在家目录下新建一个名为 .gtkrc-2.0-kde4 的文件,里面是 GTK 主题的设置:

# This file was written by KDE
# You can edit it in the KDE control center, under "GTK Styles and Fonts"

include "/usr/share/themes/Qt4/gtk-2.0/gtkrc"

style "user-font"
{
    font_name="DejaVu Sans"
}
widget_class "*" style "user-font"

gtk-theme-name="Qt4"
gtk-font-name="DejaVu Sans 9"

systemsettings 还提示需要重新登录 KDE 之类的话。但是一旦重新登录,这个 .gtkrc-2.0-kde4 就会被删除,无论把该文件的权限设置成啥样,然后 GTK 程序还是老样子。真的搞不清楚是谁跟这个文件有仇,见了它就删。

我登来登去实在没辙了,干脆直接把它改名为 .gtkrc-2.0,没想到 GTK 程序的主题立马 OK,根本不用重新登录。你说 KDE4 的方法和提示这不是害人吗?

PS: 我在另外一台电脑上发现,当先装 Ubuntu,再安装 kubuntu-desktop 时,那个 .gtkrc-2.0-kde4 是有作用的。

2. 用户帐户图片无法更改。

在 System Settings->About Me 中,提示说 Click to change your image,点击根本没用,会弹出来一个:Your administrator has disallowed changing your image. 甚至使用 root 权限,更改 root 用户的 image,也是这个提示。而 KDE4 目前没有提供一个像以前版本的 kcontrol 中进入管理员模式的按钮,所以我实在不知道该怎么办了。这个错误好像在 SUSE, Fedora, Gentoo 的邮件列表中都有报告,没看到有可用的解决方法。而且这个提示也太恶心,谁是 Your administrator?root 的 administrator 是谁?在哪里 disallowed 的?是 user privilege 不够吗?我深深地怀疑是不是 KDE4 目前没有实现这个东西,搞个提示唬人的。

PS: 我知道本文涉及到了 Linux 世界的两大战争:发行版之争和桌面系统之争,评论请就事论事,不要涉及这两点。

Revolutionary Road

周末无事,来写影评吧。

Revolutionary Road(革命之路),一部 Leonardo DiCaprio 主演的电影,在豆瓣上最经典的评价是:“如果杰克当年爬上了木板,他和露丝现在就是这副德兴。”我不太怀疑这句话的正确性,但我仍不禁产生了一个邪恶的想法:Jack 和 Rose 之所以成为传奇,成为“特别的”,勾引无数痴情小女生看完后眼泪汪汪地对男朋友说:“If I jump, do you jump?”,大概也因为 Jack 没有爬上那张木板。

一个平庸的讨厌工作的公司职员,一个梦想做演员的家庭主妇,一个靠搬家到巴黎解决婚姻问题的妄想,一次误打误撞得到提升带来的幻想,一次妄想破灭转嫁于孩子身上却带走自己生命的流产,再加上一个说话尖刻夸张却能看透人心的所谓疯子,一个默默欣赏着邻居美丽妻子的丈夫,一个知道丈夫心思却忍受许久而最终得以解放的妻子,构成了这个发生在 Revolutionary Road 的故事。

看电影最痛苦也最爽快地就是能在电影中看到自己的影子。那个打定主意要放弃自己不喜欢的工作,与妻子搬到巴黎去继续自己不平凡梦想的 Frank,忽而得到提升机会,便以为自己很擅长那份讨厌的工作,又萌生了继续工作下去的想法,并寻找各种理由来反对那个曾经同意的建议。其实就像自己,看似为自己而活,实际上却活在别人的眼光里,喜好、能力莫不是被外力敲敲打打。回头看看走过的路,别人的肯定与否往往影响着自己的选择,而不是由发自内心的喜爱或者憎恶来控制,更可笑的是,会编造出一套虚假的理由来劝说自己承认别人是对的。这样的自己,也算是特别的吗?

挣扎在平凡与特别之间的,不只有 Frank 和 April 两个人。是我们太傻,还是太聪明——能够认为 we are special?

Patch for Libjingle with GCC 4.2.4 on Ubuntu

It is a svn diff result, not a patch, actually.

So, what is Libjingle? Quoted from http://code.google.com/p/libjingle/:

Libjingle, the Google Talk Voice and P2P Interoperability Library, is a set of components we provide to interoperate with Google Talk's peer-to-peer file sharing and voice calling capabilities. The package includes source code for Google's implementation of Jingle and Jingle-Audio, two proposed extensions to the XMPP standard that are currently available in draft form.

You can check out the head revision of Libjingle from its svn repository using command:

svn checkout http://libjingle.googlecode.com/svn/trunk/ libjingle-read-only

Then ``./autogen.sh'' and ``make'' as we usually do for building a *nix software. You will find many errors during ``./autogen.sh'' and ``make''. To fix them, first, some LIBs should be installed:

sudo apt-get install build-essential libexpat1-dev libglib2.0-dev libogg-dev libssl-dev libasound2-dev libspeex-dev openssl libortp7-dev libmediastreamer0-dev libavcodec-dev

I am not very sure if these LIBs are enough. If you have some problem with this, please let me know.

Even if you have all of these LIBs installed, you will still get some errors such as:

../../talk/base/stringutils.h:272: error: extra qualification 'talk_base::Traits::' on member 'empty_str'
../../talk/base/base64.h:26: error: extra qualification ‘talk_base::Base64::’ on member ‘Base64Table’
../../talk/base/base64.h:27: error: extra qualification ‘talk_base::Base64::’ on member ‘DecodeTable’

So here is a patch for source code errors like this. IMPORTANT NOTE: gcc version 4.2.4 on Ubuntu 8.04, libortp7.

Index: talk/p2p/base/sessionmanager.h
===================================================================
--- talk/p2p/base/sessionmanager.h    (revision 7)
+++ talk/p2p/base/sessionmanager.h    (working copy)
@@ -156,7 +156,7 @@

   // Creates and returns an error message from the given components.  The
   // caller is responsible for deleting this.
-  buzz::XmlElement* SessionManager::CreateErrorMessage(
+  buzz::XmlElement* CreateErrorMessage(
       const buzz::XmlElement* stanza,
       const buzz::QName& name,
       const std::string& type,
Index: talk/session/phone/linphonemediaengine.cc
===================================================================
--- talk/session/phone/linphonemediaengine.cc    (revision 7)
+++ talk/session/phone/linphonemediaengine.cc    (working copy)
@@ -80,24 +80,24 @@
     }
#endif
#ifdef HAVE_SPEEX
-    if (i->name == speex_wb.mime_type && i->clockrate == speex_wb.clock_rate) {
-      rtp_profile_set_payload(&av_profile, i->id, &speex_wb);
-    } else if (i->name == speex_nb.mime_type && i->clockrate == speex_nb.clock_rate) {
-      rtp_profile_set_payload(&av_profile, i->id, &speex_nb);
+    if (i->name == payload_type_speex_wb.mime_type && i->clockrate == payload_type_speex_wb.clock_rate) {
+      rtp_profile_set_payload(&av_profile, i->id, &payload_type_speex_wb);
+    } else if (i->name == payload_type_speex_nb.mime_type && i->clockrate == payload_type_speex_nb.clock_rate) {
+      rtp_profile_set_payload(&av_profile, i->id, &payload_type_speex_nb);
     }
#endif

     if (i->id == 0)
-      rtp_profile_set_payload(&av_profile, 0, &pcmu8000);
+      rtp_profile_set_payload(&av_profile, 0, &payload_type_pcmu8000);

-    if (i->name == telephone_event.mime_type) {
-      rtp_profile_set_payload(&av_profile, i->id, &telephone_event);
+    if (i->name == payload_type_telephone_event.mime_type) {
+      rtp_profile_set_payload(&av_profile, i->id, &payload_type_telephone_event);
     }
    
     if (first) {
       LOG(LS_INFO) << "Using " << i->name << "/" << i->clockrate;
       pt_ = i->id;
-      audio_stream_ = audio_stream_start(&av_profile, 2000, "127.0.0.1", 3000, i->id, 250);
+      audio_stream_ = audio_stream_start(&av_profile, 2000, (char *)"127.0.0.1", 3000, i->id, 250);
       first = false;
     }
   }
@@ -106,7 +106,7 @@
     // We're being asked to set an empty list of codecs. This will only happen when
     // working with a buggy client; let's try PCMU.
      LOG(LS_WARNING) << "Received empty list of codces; using PCMU/8000";
-    audio_stream_ = audio_stream_start(&av_profile, 2000, "127.0.0.1", 3000, 0, 250);
+    audio_stream_ = audio_stream_start(&av_profile, 2000, (char *)"127.0.0.1", 3000, 0, 250);
   }
 
}
@@ -114,12 +114,12 @@
bool LinphoneMediaEngine::FindCodec(const Codec &c) {
   if (c.id == 0)
     return true;
-  if (c.name == telephone_event.mime_type)
+  if (c.name == payload_type_telephone_event.mime_type)
     return true;
#ifdef HAVE_SPEEX
-  if (c.name == speex_wb.mime_type && c.clockrate == speex_wb.clock_rate)
+  if (c.name == payload_type_speex_wb.mime_type && c.clockrate == payload_type_speex_wb.clock_rate)
     return true;
-  if (c.name == speex_nb.mime_type && c.clockrate == speex_nb.clock_rate)
+  if (c.name == payload_type_speex_nb.mime_type && c.clockrate == payload_type_speex_nb.clock_rate)
     return true;
#endif
#ifdef HAVE_ILBC
@@ -171,8 +171,8 @@
#ifdef HAVE_SPEEX
   ms_speex_codec_init();

-  codecs_.push_back(Codec(110, speex_wb.mime_type, speex_wb.clock_rate, 0, 1, 8));
-  codecs_.push_back(Codec(111, speex_nb.mime_type, speex_nb.clock_rate, 0, 1, 7));
+  codecs_.push_back(Codec(110, payload_type_speex_wb.mime_type, payload_type_speex_wb.clock_rate, 0, 1, 8));
+  codecs_.push_back(Codec(111, payload_type_speex_nb.mime_type, payload_type_speex_nb.clock_rate, 0, 1, 7));
  
#endif

@@ -181,8 +181,8 @@
   codecs_.push_back(Codec(102, payload_type_ilbc.mime_type, payload_type_ilbc.clock_rate, 0, 1, 4));
#endif

-  codecs_.push_back(Codec(0, pcmu8000.mime_type, pcmu8000.clock_rate, 0, 1, 2));
-  codecs_.push_back(Codec(101, telephone_event.mime_type, telephone_event.clock_rate, 0, 1, 1));
+  codecs_.push_back(Codec(0, payload_type_pcmu8000.mime_type, payload_type_pcmu8000.clock_rate, 0, 1, 2));
+  codecs_.push_back(Codec(101, payload_type_telephone_event.mime_type, payload_type_telephone_event.clock_rate, 0, 1, 1));
   return true;
}

Index: talk/xmpp/xmppclient.h
===================================================================
--- talk/xmpp/xmppclient.h    (revision 7)
+++ talk/xmpp/xmppclient.h    (working copy)
@@ -138,7 +138,7 @@
     }
   }

-  std::string XmppClient::GetStateName(int state) const {
+  std::string GetStateName(int state) const {
     switch (state) {
       case STATE_PRE_XMPP_LOGIN:      return "PRE_XMPP_LOGIN";
       case STATE_START_XMPP_LOGIN:  return "START_XMPP_LOGIN";
Index: talk/third_party/mediastreamer/msrtprecv.c
===================================================================
--- talk/third_party/mediastreamer/msrtprecv.c    (revision 7)
+++ talk/third_party/mediastreamer/msrtprecv.c    (working copy)
@@ -26,7 +26,7 @@
MSMessage *msgb_2_ms_message(mblk_t* mp){
     MSMessage *msg;
     MSBuffer *msbuf;
-    if (mp->b_datap->ref_count!=1) return NULL; /* cannot handle properly non-unique buffers*/
+    if (mp->b_datap->db_ref!=1) return NULL; /* cannot handle properly non-unique buffers*/
     /* create a MSBuffer using the mblk_t buffer */
     msg=ms_message_alloc();
     msbuf=ms_buffer_alloc(0);
@@ -120,7 +120,7 @@
         gint got=0;
         /* we are connected with queues (surely for video)*/
         /* use the sync system time to compute a timestamp */
-        PayloadType *pt=rtp_profile_get_payload(r->rtpsession->profile,r->rtpsession->payload_type);
+        PayloadType *pt=rtp_profile_get_payload(r->rtpsession->rcv.profile,r->rtpsession->rcv.telephone_events_pt);
         if (pt==NULL) {
             ms_warning("ms_rtp_recv_process(): NULL RtpPayload- skipping.");
             return;
Index: talk/third_party/mediastreamer/audiostream.c
===================================================================
--- talk/third_party/mediastreamer/audiostream.c    (revision 7)
+++ talk/third_party/mediastreamer/audiostream.c    (working copy)
@@ -112,7 +112,7 @@
             RtpSession **recvsend){
     RtpSession *rtpr;
     rtpr=rtp_session_new(RTP_SESSION_SENDRECV);
-    rtp_session_max_buf_size_set(rtpr,MAX_RTP_SIZE);
+    rtp_session_set_recv_buf_size(rtpr,MAX_RTP_SIZE);
     rtp_session_set_profile(rtpr,profile);
     rtp_session_set_local_addr(rtpr,get_local_addr_for(remip),locport);
     if (remport>0) rtp_session_set_remote_addr(rtpr,remip,remport);
@@ -133,7 +133,7 @@
     /* creates two rtp filters to recv send streams (remote part)*/
    
     rtps=rtp_session_new(RTP_SESSION_SENDONLY);
-    rtp_session_max_buf_size_set(rtps,MAX_RTP_SIZE);
+    rtp_session_set_recv_buf_size(rtps,MAX_RTP_SIZE);
     rtp_session_set_profile(rtps,profile);
#ifdef INET6
     rtp_session_set_local_addr(rtps,"::",locport+2);
@@ -147,7 +147,7 @@
     rtp_session_set_jitter_compensation(rtps,jitt_comp);
    
     rtpr=rtp_session_new(RTP_SESSION_RECVONLY);
-    rtp_session_max_buf_size_set(rtpr,MAX_RTP_SIZE);
+    rtp_session_set_recv_buf_size(rtpr,MAX_RTP_SIZE);
     rtp_session_set_profile(rtpr,profile);
#ifdef INET6
     rtp_session_set_local_addr(rtpr,"::",locport);
@@ -217,8 +217,8 @@
     ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_FREQ,&pt->clock_rate);
     ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_BITRATE,&pt->normal_bitrate);
    
-    ms_filter_set_property(stream->encoder,MS_FILTER_PROPERTY_FMTP, (void*)pt->fmtp);
-    ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_FMTP,(void*)pt->fmtp);
+    ms_filter_set_property(stream->encoder,MS_FILTER_PROPERTY_FMTP, (void*)pt->send_fmtp);
+    ms_filter_set_property(stream->decoder,MS_FILTER_PROPERTY_FMTP,(void*)pt->send_fmtp);
     /* create the synchronisation source */
     stream->timer=ms_timer_new();
    
Index: talk/third_party/mediastreamer/msrtpsend.c
===================================================================
--- talk/third_party/mediastreamer/msrtpsend.c    (revision 7)
+++ talk/third_party/mediastreamer/msrtpsend.c    (working copy)
@@ -85,7 +85,7 @@
{
     guint32 clockts;
     /* use the sync system time to compute a timestamp */
-    PayloadType *pt=rtp_profile_get_payload(r->rtpsession->profile,r->rtpsession->payload_type);
+    PayloadType *pt=rtp_profile_get_payload(r->rtpsession->snd.profile,r->rtpsession->snd.telephone_events_pt);
     g_return_val_if_fail(pt!=NULL,0);
     clockts=(guint32)(((double)synctime * (double)pt->clock_rate)/1000.0);
     ms_trace("ms_rtp_send_process: sync->time=%i clock=%i",synctime,clockts);
Index: talk/base/base64.h
===================================================================
--- talk/base/base64.h    (revision 7)
+++ talk/base/base64.h    (working copy)
@@ -23,8 +23,8 @@
   static std::string decode(const std::string & data);
   static std::string encodeFromArray(const char * data, size_t len);
private:
-  static const std::string Base64::Base64Table;
-  static const std::string::size_type Base64::DecodeTable[];
+  static const std::string Base64Table;
+  static const std::string::size_type DecodeTable[];
};

} // namespace talk_base
Index: talk/base/stringutils.h
===================================================================
--- talk/base/stringutils.h    (revision 7)
+++ talk/base/stringutils.h    (working copy)
@@ -269,7 +269,7 @@
template<>
struct Traits<char> {
   typedef std::string string;
-  inline static const char* Traits<char>::empty_str() { return ""; }
+  inline static const char* empty_str() { return ""; }
};

///////////////////////////////////////////////////////////////////////////////

You killed all these errors? Congratulations! You can start talking with your gtalk friends with command ``call'' in talk/examples/call/ !

PS: If you are working with GCC 4.3.x, more strict checking is applied on the code. However, most errors can be fixed by adding some C headers into the #include fields, such as: <cstdlib>, <cstring>.

Gnome-Do 和 Launchy

我很喜欢那种充满创意的小程序,特别是超出我的想像那种,大概是我的想像太贫乏,因此老有惊喜。像 Synergy, Paint.Net, JabRef, Trash-cli 等,都让我觉得,哦,还有这样的软件!最近我又发现了两个,Gnome-DoLaunchy

通常情况下,为了使用方便,无论是用 Linux 还是 Windows,我总会在屏幕的最上方建一个工具栏,里面是到常用程序的快捷方式。尤其是用 Windows 时,老是按 Win+D 显示桌面然后再找启动程序太痛苦了;Linux 的程序都可以从命令行启动,状况还好一点。

但是我没想到还有像 Gnome-Do 和 Launchy 这样的程序,让我不用从一堆快捷方式中辨别我要的程序。按 Win+Space 就呼出一个小启动框,可以打开软件、打开文件夹、打开文档、打开书签、Google 搜索、Pidgin 聊天,真的是太酷了。我非常非常喜欢,它解放了我的快速启动栏。感谢开源社区给我们带来这么好的软件。

Launchy v.s. Gnome-do

文献管理软件 JabRef 2.4.3 r2919

For JabRef version 2.4.2 with bug "Could not parse number of hits" (while searching ACM portal) fixed, please click here.

参考文献管理软件 JabRef 主页上的版本是 2.4.2,这个版本有一些 bug,最讨厌的就是无法在线搜索 ACM 论文库,老出现“Could not parse number of hits” 错误。

曾经用老版本的时候还没有这个问题,而 ACM 又是我常用的论文库之一。今天实在无奈了,就到 SF 上把 JabRef 最新的代码 check out 出来,用 ant 编译了一下,运行后发现这个 bug 解决了。因此我顺便打了个 jar 包和 deb 包,扔到了 http://share.solrex.org/ibuild/,有兴趣的同学自己去下吧。

注意:这个包基于 SVN 版本 r2919,不是 JabRef 官方发布,下载请注意,若有 bug,造成的后果请自负。关于此软件的使用方法,请看这里

PS:有同学报告说排序不可用,我试了一下,确实如此。而且虽然 2.4.3 貌似有中文的语言选项,但其实是不能选的。那我还是先解决 ACM 的问题吧,同时上传了 2.4.2 fix ACM 的版本,如果不愿意测试 2.4.3,可以选择这个。