Typeblog

@realPeterCxy

光に覆われし漆黒よ。夜を纏いし爆炎よ。紅魔の名のもとに原初の崩壊を顕現す。終焉の王国の地に、力の根源を隠匿せし者。我が前に統べよ!エクスプロージョン!

13,835 words

https://typeblog.net About Links en_US
You'll not receive email when Typeblog publishes a new post.
Please consider subscribing via RSS

我为什么写博客

这两天临近高中毕业,一直忙着写各种毕业留言。尤其是前两天某女生请我写毕业留言,我认认真真地写满了两页纸。于是某同学在『噫』了一番以后就开始问我

你为啥能写这么多呢

当时我的回答是

因为我能水,不然,我怎么写博客写到今天呢?

好吧这是真的,我确实很能水,一句话我能当三句话说,就像这样。然而我见过许多人,开始写博客,没几天就变成了有生之年系列;或者写来写去都是些碎碎念,最终也是荒废在那里无人问津。而我想我这个博客,typeblog.net, 算是在当时建立的那一批博客里面活得比较久的一个了,经历了 WordPress -> Jekyll -> Ghost, 没有沦为广告基地,也没有沦为SEO的奴隶。它活到今天,恐怕也不只是因为我能水而已。

最初决定开始写博客,完全是跟风而已。当时我初二,自己设立了一个卖虚拟主机的网站,买了另一家主机商的代理,加价卖出。后来『客户』开始多起来,我就想着学其他的主机商一样设立一个『官方博客』一样的东西,来做做所谓的『营销』什么的。于是这个博客就这么出现了。但是当时的我毕竟只有初二,说是想要『营销』,其实自己根本就不会,也只能天天发点不知道自己说了些什么的奇怪东西。况且我做的那个主机商没过多久就因为长时间DDoS而倒闭了(当时我不知道如何D回去),这个博客 差点 就这么荒废在那里了。

恐怕,为了商业目的而写博客,就算你有多大的能耐,最后也只能是这样的结局吧。

所幸的是后来初三毕业以后我入了 Android开发 这个大坑。就像我在 关于 页面里说的一样,这个时候我的想法是把这里做成一个开发博客,只发一些技术内容,以备以后遗忘。这就像很多攻城狮的博客一样 -- 专于技术。诚然,这样的博客,如果能坚持下去,也是个好主意。但是,我渐渐发现,纯技术类的博客,其实也没有什么意义。

  • 你今天写的技术难道不会过时吗?
  • 你写的技术内容没有其他人写过吗?
  • 通篇技术内容真的有人会看完吗?

所有技术类的博客,过了五年、十年以后,都会失去它大部分的价值。这样的内容,写在博客上,只会渐渐地被人遗忘,最后没落于网络的某个不为人知的墙角,期待着那永远都不可能再发生的邂逅。

单纯为了技术目的而写博客有什么意义呢?如果纯粹是备忘的话,完全可以写在自己的备忘录上;或者你是个专家,你可以写一本书专门讨论某个方面的技术问题。何必零零散散地拼凑成博客,最后自己都无法明白自己的博客到底是为了什么?这样的博客最后必定只能成为搜索引擎结果中的无限重复中的某一条。

我并不是在说博客上就完全不能写技术内容;我只是在说,作为一个博客,如果 只有 技术内容,那是十分不应该的。博客博客,就是 Web Log, 是用来记录的,不是用来备忘的。从一个纯技术的博客中,我们看不出任何一个博主的人格,看不出他的信仰、他的观点、他的生活,看不出他活生生的人的形象 -- 那我怎么知道不是一位机器人在写博客,不是某个软件在网络上随意抓取来的文字呢?

于是,今天的我,便不再因为那些而写博客了。

我的博客的头部现在写道

思想罪本身就是死亡

博客就是要拿来记录思想的。伟大的思想可能在千年后也不会过时;即便是对于自己来说,自己曾经的思想也是不会过时的存在。其实很多人应该都有过这样的感觉,就是看自己曾经说过的话会感觉当时自己怎么那么 naïve。然而,你如果没有碰巧看见过去的自己写下的东西,你就不会记得自己曾经 naïve 过;你不会提醒着自己不要再像过去一样。博客所做的事情更像是这种 -- 真实地记录着一个人如何成长,如何从小孩子变成大人,在这些年中他的想法是如何变化的,他又有过哪些新奇、好玩的想法,他实现了哪些,他改变了哪些。

换个角度来看,博客记录下了自己可能 naïve 的那些想法,而这些看起来 naïve 的想法有时候又何尝不是一笔财富呢?

中二的日子也要一去不复返了!可是中学时代的『中二』,又何尝不是一种单纯的快乐呢?想到这里,倒也有一种留恋之感。

这是我为某女生写下的毕业赠言中的一部分。现在的她看起来像一位『大龄儿童』,蹦蹦跳跳嘻嘻哈哈,一脸单纯的样子;日后再回头看自己的高中时代,一定也不免觉得自己太 naïve。对于未来的自己而言,过去的自己永远是简单而快乐的;看着当时简单而快乐的自己,看看当时自己写下的那些简单而快乐的文字,这未来的自己恐怕也会觉得很开心吧。毕竟,人在变得复杂的过程中,也总是会丢掉一些东西,这些东西便是博客要记录的,便是博客需要留下的宝贵财富。

也许,所谓永久的回忆,正是由这些小小的激动、瞬间的幸福感所造就。

有人说毕业赠言是矫情。然而,如果连这点矫情都没有,我们脆弱的大脑又如何证明这曾经活过的美好呢?

这也是我在毕业赠言中写下的。博客也往往是这种『矫情』,明明没什么好写的,却偏偏要故作多愁善感挤一点什么出来,有种为赋新词强说愁的味道。等我们老去以后,再看看过去的自己『矫情』而写下的文字,恐怕也会找到那些当初不曾注意的激动和幸福感吧。这些,便是岁月的证明,便是永远的回忆。

我为了自己而写博客。我所有的文章,即便看起来是在对别人说话,在讽刺某一类人,实际上也同时都是在对未来可能正在回看博客的我自己说话。我提醒自己不要成为那样的人;我告诉自己我现在的想法;我希望自己践行 Keep It Simple & Stupid。那都是真真实实的未来的我混在『他人』这些听众中间,看着那个同样真实的、年轻而中二的自己啊。

要不是有点『害羞』的感觉,我甚至想把那两篇毕业赠言完整地发上来;在五年、十年、二十年之后,再看看当时自己那些懵懵懂懂的简单的情愫,大概也会感觉到一阵温暖吧。

这就是在2016年5月21日这天,我所认为的写博客的理由。

对不起,我们是圣母

预警:这是一篇口水文。另外,请无视短链里的翻译,我实在想不出来要怎么用英文传达『圣母』的意思。

Disclaimer: 下文中所有提及的有关『圣母』『政治正确』『反圣母』,其前提全部是自愿、自发,受强制(e.g. 政府强制)而产生的并不是本文讨论的对象;同时,国家和政府层面上的类似圣母现象也不在本文的范围之中。

『圣母』这个鬼词,不知道是什么时候开始出现的,我也不知道是什么时候开始被赋予了一种奇怪的意思,搞得我都不知道要怎么翻译。不知道从什么时候起,我在知乎之类的地方只要回答『XXX(某类人或动物)也有生存的权利』,几乎立刻、马上、瞬间就会出现一大帮『站队』一般的评论 -- 圣母婊、圣母婊、圣母婊。我一开始看了以后还很生气,还尝试过反驳,可是后来发现这种回答纯粹就是贴标签,无论如何反驳他们也只会再用『圣母』这个词来反驳你,根本就是对牛弹琴,毫无作用。

这我就觉得很奇怪了,追求平等的生存权,什么时候就变成了『政治不正确』一般的东西了呢?

我不知道有些人是不是看多了所谓『政治正确』,然后开始反感这种政治正确,凡是与它有类似观点的,他们都要当作异己排斥。然而有句话说

『政治正确』还远远没有完成它的使命

当很多人还没有获得平等的生存权的时候,当很多人还处在被大众歧视的境地的时候,我不明白这种『圣母』观点何错之有。你们成天鼓吹『有疾病的都该去死』『心理疾病都是咎由自取,不应该给予治疗』,这种嘴脸,恕我直言,我想吐,吐你一脸的圣母。从某种角度上来说,人类文明的整个发展历程,就是从『野蛮』向『圣母』发展的过程。我不知道有些人是不是想回到最原始的状态 -- 平均寿命30岁都不到,病了、伤了只能听天由命,没有其他人会关心你的死活。你看,这样的社会不就是没有『圣母』存在的社会吗?可是有些人活在这里,恐怕只能是早早夭折的命运吧。

ナイーブ!アイアムアングリー!

今天我就要得罪你们一下。你们这些『反圣母婊』啊,还是需要学习一个。你们一边享受着『圣母』们前赴后继代代努力给你们带来的平等待遇,一边却装作有学识的样子到处指责别人『圣母』,我不知道这是你们真正的观点还是纯粹道听途说处处站队?如果是后者,那我倒要劝你们不要『见得风,就是雨』。你们就自己想想看,没有了『圣母』,你能活到现在吗?你是体格健壮,毫无疾病,自己搬砖都能养活自己吗?如果不是,你怎么还没去死呢?!哦,你们整天鼓吹你们自己的『决定权』,有事没事就『决定』他人的生死,中央的决定权也是很重要的你知道吗?Naïve!西方的哪个国家我没去过?隔壁房老板,比你们不知道高到哪里去了,我和他谈笑风生!落井下石的事情,你们倒是挺熟练的,我也不知道你们到底干了多少次。

无法否认的是,『圣母』也有不能圣母的时候,比如说吃饭,总要伤害到一些生命。这就像『反圣母婊』们不能时时刻刻都『反圣母』一样。实际上这就变成了一个大哥别说二哥的问题 -- 双方其实都是半斤八两的水平。我上面『怒斥』你们这一段,马上就可以有人再写一段来『怒斥』我。这样互相站队嘴炮又有什么用呢?除了浪费生命,就是浪费生命,不过『反圣母婊』们好像并不在意浪费生命,哪怕这生命是他们自己的。罢了罢了,不如好好地做好自己就够了。

我们总是避免不了弱肉强食,可是『圣母』将永远是人类文明发展的方向,如果不是,那就是在开倒车,再不然就是人类文明被毁灭了。『圣母』是『人性』的不可分割的一部分。我以为,当一个人受伤后受到同伴的照顾时,文明就产生了。

再写下去,也没什么意思了,倒反而觉得我自己也在站队。然而,有些人不跟我抱着讨论问题的态度,我又怎么反过来跟他们讨论问题?也只能反过来站队了。我也不想再在知乎这样的地方发表什么观点了,任由你们去吧,等你们把现在的『政治不正确』变成了一种『政治正确』的时候,你们还会反过来反对由你们自己造就的『政治正确』吗?

够了够了,我也不想再多说什么了,你们开心就好。毕竟,被你们害死的,暂时还轮不到你们自己。我今天算是得罪了你们一下。

使用UML合租VPS

我是从 微林 那里发现阿里云的新加坡节点的。那个时候我在用 Conoha,因为它到国内的线路越来越玄学,从以前的70ms延迟飙升到300ms,丢包率极高,所以我到微林那里去寻求一个中转服务。我本来只想用那个阿里云香港节点,但是正好看见不知何时他们加上了一个阿里云新加坡节点。因为香港的流量费实在太可怕,我就抱着试试看的心态开了一个新加坡的中转。谁知道这个中转的效果非常好,因为这个新加坡节点到中国电信有双向的CN2线路连接。然而微林的流量费还是太高:阿里云的官方流量价格只有0.53元/GiB,而最低配置的主机价格也就90元的样子。使用微林中转,我仍然需要一个国外VPS来搭建网站,倒不如直接使用阿里云提供的服务……

合租

然而,对于我这个学生党来说,94元/月的配置费仍然是高了一些。所以我想到了合租,几个人可以稍微分担一下配置费用。然而要合租的话,就需要想办法分割各自的用户空间。我首先想到的是用 LXC 或者 systemd-nspawn, 但是使用这些的话,各个用户都必须共享同一个内核。而阿里云这东西比较奇葩的是不允许自定义内核,这样总让我觉得有些不舒服。

在虚拟机里再跑全虚拟化的虚拟机显然是不合适的。而我以前也在 HawkHostOpenVZ VPS 上使用过 User Mode Linux,显然,这是个比较好的解决方案。

于是我找了两个朋友, ToukoBroncoTc,共同使用一台阿里云新加坡VPS。正好,之前我那篇关于UML的文章被我删除了,我也就可以用这篇补上这个空白了。

User Mode Linux

于是首先应该配置好 User Mode Linux, 简称 UML

User Mode Linux 是一个对Linux内核到其自身的适配。它可以将一个 Linux 运行在用户空间,同时却具有几乎全部内核支持的功能,包括但不限于自己的文件系统、自己的TCP/IP协议栈。当使用 OpenVZ 这类的VPS,需要自己挂载文件系统或使用 Tunnelbroker 一类的东西的时候,UML就十分有用了。而阿里云也不让自己换内核,即使不用和其他人合租,UML也是十分有用的东西。

因为偷懒,我在 ArchLinux AUR 上找到了一个UML的 PKGBUILD,从里面拿出了内核配置文件,打算直接使用。然而,这个内核配置文件存在一些问题,比如说关于 Netfilter (iptables) 的内核配置全部没有打开,文件系统的支持也不是很完全。所以,我们得手工编辑这个配置文件,打开需要的选项。当然,使用内核自带的图形化配置工具,也是可以的。

我在 kernel.org 上选择了最新的LTS版本 4.1.19,下载下来,解包,然后把那个改好的配置文件放了进去。然后,直接执行

make ARCH=um vmlinux

就可以获得一个名为 vmlinux 的可执行文件,这就是 User Mode Linux 的可执行文件了。

Bootstrap

然而,在使用UML之前,我们还必须先为UML配置一份自己的用户空间,即为UML虚拟出来的环境安装一个发行版。

由于UML可以从文件内的文件系统镜像启动,所以我创建了一些文件作为 虚拟磁盘 使用

fallocate -l 10G image
mkfs.xfs image

接下来就得把它挂载到一个临时挂载点

mount image /mnt
cd /mnt

以安装 ArchLinux 为例,首先得下载它的 bootstrap 压缩包然后解压。对应的bootstrap包可以在各大 ArchLinux 镜像里面找到,往往和 ISO 文件放在一起,如 http://mirror.rackspace.com/archlinux/iso/2016.03.01/archlinux-bootstrap-2016.03.01-i686.tar.gz

tar zxvf bootstrap_file.tar.gz
mv root.x86_64/* ./

如果要使用 Debian, 则上述过程都可以通过 debootstrap 脚本来完成,只要指定目标目录是挂载出来的 /mnt 即可。

此时需要将必须目录映射进挂载出来的磁盘

mount --rbind /dev /mnt/dev
mount --rbind /sys /mnt/sys
mount --rbind /proc /mnt/proc
mount --rbind /tmp /mnt/tmp

然后执行更新软件包之类的操作。接着需要配置好root密码、配置好locale等。在编辑 fstab 时,请注意,根磁盘应该是 /dev/ubda,这将在稍后的UML启动命令行中体现。

这里有一个大坑:千万不要在UML内系统的fstab里添加swapfile! 这会导致 udev 在启动时卡死。如果一定要用swap,请写一个开机启动的服务来启用。

Networking

UML的网络配置也是一个坑。要对UML启用网络,我们必须使用 tap 设备。例如,我们可以创建一个 tap0:

ip tuntap add tap0 mode tap
ip addr add 192.168.1.1/24 dev tap0
ip link set tap0 up

注意,在阿里云上,只有 192.168.0.0/16 这个内网网段是可用的。这里我们把主机IP设为 192.168.1.1, 假设我们将把UML客户机的IP设为 192.168.1.2

稍后启动UML时,我们将通过这个参数(假设MAC地址设为23:33:33:44:66:66)

eth0=tuntap,tap0,23:33:33:44:66:66,192.168.1.2

来将网络设备映射到UML内部。

为了使UML能访问网络,我们得添加 MASQUERADE 规则。在阿里云上,公网设备是 eth1

iptables -t nat -A POSTROURING -o eth1 -j MASQUERADE

我们还需要映射一定范围的端口到UML内部,以便外网访问。

iptables -t nat -A PREROUTING -i eth1 -p tcp --dport 10000:20000 -j DNAT --to-destination 192.168.1.2

UDP也同理。请记得把UML内部的 sshd 监听的端口改到这个范围之内!

对于 80443 端口,如果只有一个人使用,那么把它映射进UML是没有问题的。如果要多人使用,那就只能依靠主机 haproxy,按照域名来区分了。这要参考 haproxy 的SNI配置,我不再赘述。有一个小小的Tip,那就是在 haproxy 配置中可以使用 hdr_end-m end 指令匹配域名尾部以达到泛域名匹配的效果。

建议将这些网络设备的启动脚本加入主机的启动服务。在UML中,只需要配置 eth0 上的网络,把网关设为 192.168.1.1 就可以了。

[Match]
Name=eth0

[Network]
Address=192.168.1.2/24
Gateway=192.168.1.1

这是 systemd-networkd 的配置。

Boot up

现在已经可以启动UML了。首先我们需要把刚刚挂载出来的目录全部卸载,这一步推荐通过重启完成。然后可以启动

/path/to/vmlinux ubd0=/path/to/image eth0=tuntap,tap0,23:33:33:44:66:66,192.168.1.2 mem=512m con=pts

内存被限制在了 512m。如果你的UML中运行了 sshd, 那么你已经可以用暴露的端口访问内部运行的系统了。如果出现问题,你也可以直接通过 screen /dev/pts/X (X是一个数字,自行ls查看) 来获得一个客户机的 Login shell

当然,我比较推荐的是把这些过程全部写成一个脚本,并且让它能够读取配置文件,这样再将它包装成一个 systemd 服务,就可以方便地管理多用户和开机启动了。

流量监控

在多人共享阿里云时,流量是个大问题,因为阿里云按照流量计费。因而,我们需要一个好用的流量统计工具。

我看中的是 vnstat, 因为它可以单独统计每个网络接口的流量。而UML的每个实例又单独享有一个 tap 接口,这就使流量统计非常方便了。

在CentOS上安装和启用非常简单

yum install vnstat
systemctl start vnstat
systemctl enable vnstat

过一段时间以后,使用 vnstat -q 即可查看统计数据。当然,为了方便其他用户查看,我还利用 bashttpd 搭建了一个服务器,调用 vnstat -q -i tapN 来返回各个接口的统计数据。这十分方便。

bashttpd.conf 中只需要这几行

serve_vnstat() {
    add_response_header "Content-Type" "text/plain"
    send_response_ok_exit <<< "$(vnstat -q -i tap$2)"
}
on_uri_match '^/tap([0-9])$' serve_vnstat

然后按照官方文档使用 socat 启动服务,即可在 YOUR_URL/tapN 访问到对应设备的流量统计。

后记

虽然我们总不太喜欢BAT这类公司,但是阿里云新加坡的这个线路是真的很良心,在价格比较低的同时拥有货真价实的双向CN2,在中国国内的速度十分可观,尤其是电信。对于很多地方的联通,速度也在能够接受的范围之内。而移动,自然是不用说了。

使用UML,不仅仅可以分担费用,还有一定的防止 云盾 抽风的能力,因为云盾目前并不能透过UML控制其内部的环境。虽然并不是说有多安全,至少不会发生云盾炸掉整个环境的糗事。就目前的配置来看,即使外面环境挂了,只要UML进程还在运行,就不会出现任何严重问题。

被出卖的安全

前两天支付宝又闹出了个 隐私门 事件,说是支付宝Android版在不该用到照相权限时就申请了这个权限。根据XPrivacy拦截到的API调用过程,支付宝有偷偷上传用户隐私的嫌疑。不久以后,支付宝官方『辟谣』,几句话把这些事情从自己身上撇得一干二净,撤掉了服务器上的热补丁,然后有阿里员工开始『悬赏』请『大牛』给出流量分析。

由于吾辈日常冻结支付宝,所以我无从得知,支付宝究竟有没有做什么见不得人的事情。但是,支付宝在这事之后的反应,实在令人细思恐极。无论如何,支付宝在不应该用到摄像头的时候就提前申请了权限(Android 6.0),这是不争的事实。然而面对这样的质疑,支付宝不但不对它作出合理的解释,反而处处回避这一问题,反应激烈到无法理解。问题的核心不是在于支付宝有没有泄漏隐私,而是支付宝为什么要提前获取这个权限,提前拿这个权限去做了些什么,可是支付宝官方却一直对此避而不谈,只知道一遍又一遍地说『窝是清白的』,这就仿佛孔乙己睁大眼睛在说『你怎么凭空污人清白』。私以为,这实在不是一个 重视用户利益大厂 应该有的态度。

再之前,苹果和FBI就『是否应该放置后门以便解锁手机』的争端,也是围绕这样一个话题。

有这么一句话

《1984》是为了警示世人而写,不是TM操作说明

虽然这句话不是原作者说的,但是它的确告诉我们这样一个事实:这样『反乌托邦』的被称为 fiction 的未来似乎正在变成现实。在某恐怖组织在欧洲制造恐慌之后,甚至连个人使用『加密通讯』的权利都差点化为泡影。

那些事件以后,有人说,个人的这些所谓安全实际上都在为恐怖活动创造条件。况且,如果个人没有做什么违法的事情,那也不会有国家机构对你的什么隐私感兴趣,因而对于政府的隐私权似乎是不必要的。然而,人们对于自己的隐私安全的关注,从来都不在于到底有没有人看,而是在于有没有这样的可能被看。设想下面一个场景:你走在大街上,突然被扒光了,而你不知道是谁干的。这时候你的第一反应是什么?是的,也许大街上没什么人,就是有那么几个人也不会闲得无聊对你感兴趣,但是你仍然会想要躲起来,至少先重新换好衣服,再想其他的事情。而所谓的后门,所谓的『让政府能够获取的隐私』,虽然不是让你全裸,只是让你穿裙子而不许穿安全裤,但是这就够了吗?是的,只有某些人知道你『没穿安全裤』,但这不代表只有他们能看见。只要你走在大街上,其他人,有意或者无意,都可能发现这一事实。这样的话,和你在大街上突然被扒光,有什么两样?而某些人所想要对个人隐私干的事情,和这样的hentai行为,也没什么两样了。

在现在这个所谓互联网时代,对你的信息虎视眈眈的,可绝对不仅仅是这些有着特殊权力的机构。比如说,作为网络运营商,他们就能够通过技术手段劫持你的未加密流量,进而强制插入广告,甚至记录你的隐私。这些年来,中国几大运营商的这种劫持行为也越来越猖狂。这也是为什么我的博客要加上全局 HTTPS, 还通过 HSTS 强制使用加密链接。这绝对不是因为我的博客涉及到什么重大机密、重要隐私,而是要对每一个访问者负责,每一个人在网络上看到的信息都应该是由真实的原发布者所发布的原信息。失去了通信秘密的权利,你连自己的网络连接流量发向了哪里都不可能知道,又谈何隐私,谈何安全?

退一万步讲,即使禁止个人使用无法被加入后门的加密算法这种东西被写入了法律,就真的能够杜绝那些恐怖分子的秘密通信了么?我看未必。法律起作用的前提是行为的实施者坚持遵守法律。而恐怖分子是什么样的人?他们如果是法律的遵守者,还可能想到去发动恐怖活动吗?人家都是恐怖分子了,违反一两条法律,和杀一群人来制造恐慌,哪个更严重?而后者对于他们来说已经习以为常。即使不使用复杂的加密算法,他们也有一百种方法在某些人的眼皮子底下传递信息,而这些人却无可奈何。如果这种条文被写入了法律,那就只有犯罪分子才拥有通信秘密了,这难道不令人感到十分可笑?不止是可笑,更是可悲,可悲某些人到了21世纪,还连这么简单的道理都想不明白。

其实,我倒是想反过来问问这些鼓吹应该禁止个人使用无法破解的加密的人。你们啊,连几个逃犯都抓不住,就想着要大家都把自己的底裤暴露给你看,这真是naïve。连几个牛逼的安全专家的工资都不肯付,就来吐槽没法破解所谓犯罪分子的加密信息,要手机生产厂商专门为你植入后门,也真是无能。你开不了防盗门,就让所有人都不要装防盗门,这种嘴脸……一谈到如何保护隐私,就跑,跑得比香港记者还快。

谁出卖了我们的安全?有时候,甚至可能是我们自己的所谓『安全意识』的薄弱。当然,我绝对不是在说,所有人都要用尽可能高级别的安全措施,因为毕竟安全和方便是个鱼与熊掌的问题,我们更多的还是需要方便。但是,当面对支付宝这样的可疑的权限申请的时候,普通人就不应该保留一分疑问吗?只有当大多数人都不能容忍这样的行为的时候,这些大厂商才会意识到对用户负责的重要性,某些权力机关也才会认识到自己以前的愚蠢行为。

我只是希望,当你在网络上随便一搜就能发现自己的裸照,自己和另一半干某些羞羞的事情的照片/视频的时候,你不会后悔道:

当初,我要是拒绝了支付宝的权限申请就好了。

结束的开端

2016年2月19日,距离这个寒假结束还有一天不到。这是我高中生涯中的最后一次寒假,也意味着这一切将在不到四个月的时间里结束。

是啊,不到四个月了呢。我在中考完的那个暑假,曾经有这样一种感觉,就是高考什么的,还有三年,远着呢。远着呢,远着呢,这句话还如此清晰,我却已经站在这个结束的开端。

假期往往是我的痛,因为无论在假期前许下什么样的『要好好学习』的誓言,在假期中都会被丢到九霄云外。不管怎么样,我总会想到那些在开学期间被丢到一旁的项目,想到那些我自己都未必看得懂的自己写的代码,和那一堆难以逐一清理的用户反馈和崩溃报告。而每个假期,总是在这样的纠结之中度过的。每次我意识到这种纠结,往往已经处于假期的终结。也许正是因为这样,我始终害怕开学,害怕将要遭遇的那一切。就在这样一次又一次的纠结之中,我已经面临着高中的最后一个学期,最后的这点时间。

我这人对于像高考这样,因为一点点的不同的念头就可能改变以后的一生的事件,一直有莫名的恐惧感,虽然我明明知道即使没有这么大的事件,人生也可能在一念之间完全不同,所谓『蝴蝶效应』。现在想想,也许使我真正感到恐惧的,不是这种事件本身,也不是可能改变一切的选择,而是『结束』。

之前在知乎上看见过一个回答,里面有这样一句话

从古至今,人类所恐惧的,不是其他,而是“永远”

而有些时候的『结束』正是意味着『永远』。当这最后几个月过去,我将再也没有机会坐在这样的教室里,听这样的课,也许也再也遇不到这样看似成熟但实际上还是有些 naïve 的同伴。而我之所以恐惧作出选择,恐惧它们对未来可能招致的改变,正是因为我将再也没有机会去改正这一切。我爱过的人和爱过我的人,我伤害过的人和伤害过我的人,也许正会随着这12年的生活的远去而远去。也许,从一开始,我所有的恐惧都是出于这种原因。

寒假补了很多番,前两天刚看完『寒蝉鸣泣之时』。Rena酱的一席话很有深意。她说道,判断两个世界好与不好,那是『神』才需要烦恼的事情。而人本身,并不能跨越世界,也因此无法知晓每一种选择带来的后果,所以,为此而烦恼有什么意思呢?

不过,圭一君也说过,命运是可以改变的。然而,可以改变命运,并不意味着人都应该为了这样的没有来到的未来而烦恼。既然明知道要结束,那『结束』这一件事情就几乎是个固定的事件,没有办法去改变。人都不是神,更没有羽入那样的起死回生、回溯过去的能力。寒蝉中的梨花,之所以最后摆脱了这样的悲剧循环,一方面是朋友的合作努力,另一方面也离不开她能够不断循环这一段生活,从而找出整个事情的真相。若她并不具备这样的机会,那么要摆脱这种命运的几率就太小太小了。而这正是对于我们其他人的情况。『神秘博士』中也不断提到过『时间定点』的观点,有些事件,无论你如何尝试,它也会至少换一种形式发生,如果不发生,就可能导致其他意想不到的灾难。

然而博士也经常说,这并不意味着什么也改变不了。至少,开个小玩笑,还是允许的。也就是说,我们至少能够掌握在这个终结到来之前的那些日子。所谓的『改变命运』,我想,改变的就是这些。对于梨花来说,至少她还有几个星期的时间和朋友在一起构筑友谊;对于我来说,至少,还有四个月不到的时间。一些很小的事情是能够改变的,然而这些很小的事情,对于同样渺小的人来说,很可能就是不同的命运了。

如果从一开始就在害怕结束后的事情,那也许我从一开始就输了。尽管我站在结束的开端,但这毕竟也还算一个『开端』,不是吗?最后结果如何,说实话,很少有人能在这开端就预见到,因为一念之差就可以造成不同的结果。但,至少在看到结果的时候,能够告诉自己,这是属于自己的结果,而不是自己从一开始就害怕所换来的几乎完全『随机决定』或完全由自己的恐惧情感决定的结果。

命运存在不存在?我不知道,但我知道,在这结束的开端,『选择的机会』至少还是存在的。在高中生涯结束之前,我还有三个多月的时间,那是结束的过程。过去已经是过去,就算是句点,也要画成最接近完美的圆。

我们挥手 终究却忘了牵手

鼓起勇气却只能做朋友

犹如花火般短暂的美梦

我庆幸我 也曾感受过

一万次的道别 难道还不够

也许再见 只是一个承诺

你在夕阳里挥手的轮廓

直到现在 我依然记得

原来最美的话 在于不说

承诺在于 我们 都忘了

没有翅膀的人

事情是这样的,今天微博时间流里出现了一个名叫『崇才科技』的自称『第一00后团队』的公司。光是看这条消息,吾辈就被吓了一大跳。于是我随便下载了个他们 网站 上贴出来的『得意之作』


唔,吓尿了。当然,BeetMan 同学已经在 这里 抢先于我吐槽过了,我呢,只是想再说两句。

说实话,我想要笑话他们,但笑话他们会让我觉得在笑话自己。谁在这个年龄不会想要做一些这样的事情呢?不过是想要证明自己,想要向世界宣告自己也有大人一般的实力罢了。我当年玩『易语言』的时候,也干过不少类似这样的事情。我曾经 Copy-Paste 别人的代码,改一下UI,就说是自己的作品,也无非就是为了所谓的成就感。对成就感的追求是无可厚非的,即使被称为中二病,至少这可以告诉人们有这样一个人曾经活过。

『美丽新世界』中就不存在『中二病』。这本书里面描述的人们,从小就受到睡眠教育,将所有有关改变、有关创造的事情都排除在『快乐』之外,以此保证社会的『稳定』。代价是,整个人类的科技水平都被自己锁死在一个固定的水平,因为新的发现不利于稳定;人类的文化与艺术毁灭殆尽,因为如果人们不再需要成就,不再有进取的意识,那么任何知识、思想都成了不必要的。显然,这样的一个世界是反乌托邦的存在,而其反乌托邦的一个重要原因就是『中二病』不再存在,所有本该中二的人都沉迷于虚假的快感之中。这种虚假的快感,代替了本应该由成就、由爱、由友情带给人的快乐。

如果人不再中二,都足够理性,我们将永远被固化在现在的文明状态。因而,我不想去笑话他们,这可能对他们造成无法逆转的打击,可能会就此毁灭他们对与众不同,对创新,对个性的追求。可是,有的人恰恰与此相反。他们看到了这种『中二』背后的商机——只要把这种中二往某一个方向加以引导,那么这种中二就很可能可以为某些公司的商业利益服务,因为这种中二往往是不顾一切的,他们只想证明自己能做。只要不断地用这种糖衣炮弹对他们加以诱惑,他们就会不知不觉地走上一条不归路,一条成为别人的提线木偶的不归路。证据还不够明显吗?铺天盖地的水军,肉麻不堪的软文,无底线的吹捧……这无论换作是谁,都无法完全抵御。于是,一个木偶公司在他们手中诞生,一个完全不知道自己的真实水平,只会骗取投资的公司就此诞生。

这样的开发者是否还有救?我不知道。这两百名所谓员工,几乎就像进了传销组织一样,再也无法看清庐山真面目。他们的眼里将只剩下软文,只剩下那些不着调的 Tree New Bee, 而真正的开发技术,恐怕早就被抛到九霄云外了吧。

BeetMan 说道

你们连给他们买本符合实际的Android开发书的钱都不肯花么?连给他们买个域名买个空间的钱都不肯花么?

很可惜,这些商业公司啊,只看中了『中二能创造利润』这一点。这利用中二啊,仿佛就是挤奶一般,挤出奶的奶牛一点都不重要,重要的是能卖钱的牛奶。奶牛之所以还有那么点重要性,不过是因为它还能挤出牛奶。若是挤不出奶了,那它们就别无选择,只能面对死亡。反过来想想这些00后的命运,呜呼哀哉!呜呼哀哉!

最近看了个番,叫『我们没有翅膀』。我想,我可以把『翅膀』这个概念借来用一下。中二就是一个人的翅膀,它带领人飞起来,使人有可能飞向梦想的彼岸。然而有的人看中了这美丽的翅膀,便像引诱白雪公主吃苹果一样引诱他们,使他们获得短暂的飞得更高的乐趣,却在不知不觉间被一点点地夺走了翅膀上的羽毛。如同温水煮青蛙一样,毫无感觉的他们只会在某一天突然发现,自己已经成了没有翅膀的人。

我只想说,愿人人都还有翅膀。

盛世除夕

大年三十高高兴兴刷微博,却被一条评论弄得兴致全无

其实这PO只是吐槽了一下今年春晚上出现的几个问题,还没有其他人喷的厉害,却招来了这样一批人。当然,我随便戳开了他们的微博看看,发现都是某男子三人组合的粉丝,那么这也就不奇怪了。

但他的这一番话,却无法不让人细思恐极。

正能量

评论里有这么一句话

生活在这个祖国的人民还是需要正能量的。

于是我随便查了一下正能量

正能量是什么?Google说,是乐观,是积极,是一种心态。

互联网不仅是化解民怨的减压阀,更应该成为国民心态的压舱石,发掘草根中的真善美,重新提振诚信,传递正能量。

Wikipedia 上引用人民网的话如是说。这句话显然有很直接的政治目的,但这些我不管。我只看见了三个字:

真、善、美

我不知道从什么时候开始,这样的事情在某些人眼里变成了真善美。这样在全世界的华人面前弄虚作假,这样侮辱作为『祖国的希望』的孩子们的智商,这样拙劣的编排,如此做作的煽情,什么时候变成了真善美?满眼我也只能看见三个字:

假、大、空

当正能量变成假大空的同义词的时候,我们的未来也许就会变成春晚的那个样子。即使弄虚作假,为了所谓的吉祥,人们即使发现了问题也不会去指出;为了不让人泄气,所有人只去赞颂美好。

然而,如果说这样是假大空的话,那句话好像就自相矛盾了诶。

赞颂 = 葬送

从这个评论者的逻辑来看,如果我们对某件事情有所不满,我们将会使人泄气,从而导致正能量的缺失。

当所有人一齐赞颂美好,我们将深陷『乌托邦』之中,一切在我们的眼中都是完美的、合理的,不存在比目前更好的解决方案。你目前所处的境地,所处的社会阶层,所受到的待遇,所获得的服务,全部是公平公正合理的,你不可能因此有任何异议。从社会的最底层,到国家元首,没有人会做出不正确的决定,所有政策无不是经过了不能再科学的研究,考虑了所有可能考虑的因素才决定的。所有人安居乐业,不必考虑任何事情,不必奋斗,不必担心,只要平静地度过这一生就好。

这样的乌托邦是不是充满了正能量?可是,如果让你去这样的乌托邦中生活,你会愿意吗?

很多人会这样说

这样的生活有什么意思?

是啊,有什么意思?人的天性就是不能安于现状,就是要寻求改变,思考自己遭遇的一切是何种含义,是否是自己本就应得的。我们的先辈们经历无数次政权更迭只是为了改变一批又一批人命运,人类发明了无数工具不过是为了在世界上活得更好。人类记录历史是因为没有人能够保证自己的抉择万无一失,所以需要历史作为镜子,只是大部分人从来不照罢了。人们考虑着这样或那样的事情,不过是为了活出存在的意义,而不是和动物一样在不知不觉间蹉跎了一生。

我就是我,是颜色不一样的焰火。

如果人们都被这样的正能量概念所困,开始赞颂一切,我们将开始失去所有造就今天的一切品质。你将不再会思考,不再会提问,命运将会变成一个真正的定值:仅由出身决定。地球将如同一个不会转动的死球,而对于每个人自身,他们也将失去自己本身的存在。

『绯弹的亚里亚』中的理子说过这样一句话

理子就是理子,不是什么罗苹四世。

人对其他事物持怀疑态度,有自己的看法,甚至颇有微词,这些都不是什么令人泄气的东西。恰恰相反,正是这些东西在催人奋进。拿我自己来说,我也算是做过几个App项目的人了,那么,我是靠什么来不断改进这些?难道只靠我自己吗?不可能。我不止一次有过这样的体会,就是自己发现自己的App存在不足时,自己会对自己说,“不是什么大问题,也就稍微麻烦一点”,就懒得去修正了。但是在别人看来,这也许正是一个致命级别的bug。在这种时候,若是缺了别人的不同意见,自己的所谓正能量就变成了令人泄气的能量了。

当人人都在赞颂未来时,我们将葬送自己的未来。

年味

所谓的年味也正是如此。

有的人说,现在的年味没有以前浓了。但我想说的是,现在平时的物质生活条件好了,所以年味中那些过于物质的部分在逐渐被淡化,这剩下的年味,才是真正的年味。

年味是什么?抢红包?吃肥猪肉?放鞭炮?这些若是想做,什么时候都能做到,并不需要在过年这一特殊时间点上。但过年的时候你得回家吧?即使不回家,也要有许多距离较近的亲戚在一起聚一聚吧?在平时,所有人都莫名地忙碌,根本不会想到要聚一聚说说话,倒反而只有在过年的时候才能做到。

醒醒吧,那种大家一起祈祷来年丰收的年味已经消失殆尽了。那种年味,对于所有人来说都是一样的,无非就是吃饱肚子穿暖身体,没有任何特殊性。没有任何特殊性,它就不是真正的年的味道,就像我刚才所说的,这样的 正能量 是一种幻觉般的存在。这样的祈祷是脆弱不堪的,一旦来年有一天不是那么顺利,它就会立即化为泡影。而实际上,当我们许下这种愿望的时候,我们早已知道了结局。我们知道了结局,却迫于其他人的压力而不得不说一些吉祥的话做一些吉祥的事情以得到所谓的年味。这样的年味,永远都不是年味。你即使对它再怀念,印象再深刻,也不是年味。它一触即碎。

你可以尝试一年不吃肉不吃油甚至连盐都少到最低限度,然后在那一年的除夕大吃一顿年夜饭,开始期盼在将来的一年里不需要忍受这样的苦痛。这并不值得,你不值得主动去用苦痛换取一刻的快感。真正的年味绝对不是在期盼未来或者回味过去中品尝的。赶快醒过来,看看你的周围。也许你的身边正坐着半年甚至一年都没见到的父母、亲戚或者老相识,甚至可能有你的青梅竹马,你们就不想叙叙旧?不想互相交流交流?这一年就没遇到什么新鲜事要和他们说说?你真的觉得鞭炮什么的所谓的有年味的东西能和这样的人们划上等号?年实际上给相隔千里的人们建立了一条纽带,年味也正是由这条纽带的材料--情感--所构成的。

当年味的所有糖衣全部被剥光,它将赤裸裸地面对我们。那也许不再诱人,但它一定是人类自诞生以来永远不会改变的东西,那就是情感,是亲情,是友情,是爱情。

致春晚

扯了这么多,还是回到主题吧。

我实在想对春晚的节目组说,我们需要的是更加真实的年。真实不是指不能失误,真实甚至可以容许失误,因为真实的要求只有真实。我们所需要的正能量,不是去歌颂什么大恩大德,而是去发现我们还未解决的问题,然后呈现出来,引发大家的共鸣,从而使大家共同思考改善这些状况的方法。真正让人泄气的,就是这些不真实的、做作甚至下作的煽情,是得不到共鸣的所谓正能量。

如若春晚一直这样下去,那么这档经典节目只能是吃枣药丸,而这个神奇的地方也只能永远神奇下去了。若是这样,我也只能说这么一句

这盛世,如你所愿

给Ghost博客系统添加中文字数统计

我在前几篇文章中已经提及,最近我将我的博客系统切换到了 Ghost。我已经夸过它的不少优点,如轻量等,但现在的 Ghost 有个非常致命的问题,就是不支持统计中文字符数量。无论你在一篇文章里写了多少汉字,它自带的字数统计总是按照空格数量进行,也就是英语习惯。而中文的字词之间没有天然的分隔,所以这种统计自然就会不准确。不仅仅是中文,对于中日韩语言来说, Ghost 的这种统计方式都存在很大的问题。

方案

首先我在 Ghost 的repo里找到了 Issue #2656Issue #3801。在这两个议题中,他们提到的解决方案是首先判定语言,然后使用对应语言的分词技术来统计单词数量。起初,我也认为这个方案是可行的,于是在 Ghost 源码里一番折腾。我找到了 core/client/app/helpers/gh-count-words.js,这里有这么一段代码

let markdown = params[0] || '';

if (/^\s*$/.test(markdown)) {
    return '0 words';
}

let count = counter(markdown);

它首先判断是否为空,然后将整个编辑器的文本传递给字数统计函数进行统计。我本想从这里入手,用正则表达式提取文本中的中日韩字符数量,因为 Ghost 已经默认引用了 XRegExp, 它支持完整的 Unicode, 只需要

\\p{Katakana}\\p{Hiragana}\\p{Han}\\p{Hangul}

就可以提取出CJK字符。统计它们的数量,如果在总长度中占某个比例或以上,就可以认为这篇文章的主要语言是中日韩的一种,然后再调用分词技术解决问题即可。

做到一半的时候,我意识到了一个非常严重的问题,那就是似乎不能使用这种方式来判断文章的主要语言。特别是对于我的这种经常贴代码的博客,一篇中文的文章中常常夹杂比中文内容更长的拉丁字符。在这种情况下,如果按照长度判断,就完全不可靠了。而如果按照后台设置的语言来决定是否使用CJK字符统计逻辑,那还存在一个问题,就是现有的分词手段的内存消耗非常恐怖,我想没有人会愿意开个博客的编辑器都要等半天让浏览器载入分词数据库……

这个时候,Orz大水群里有人告诉我,即使是在 MS Word 中,东亚字符也是单独被统计的,有单独的一项显示东亚字符数量。如此看来,我也不如退而求其次,当中日韩文字存在的时候,只要在字符统计处多显示一项,专门显示东亚文字的数量,也就足够达成目的了。

Code

实际上,Ghost 的后台的模板和前台的模板语法几乎是一样的。在最新的 master 分支中,编辑器的模板被放在 core/client/app/templates/components/gh-editor.hbs,其中

<span class="entry-word-count">{{gh-count-words value}}</span>

一句的作用就是调用 core/client/app/helpers/gh-count-words.js 这个helper来显示字符数量。于是我就仿照这个helper的做法,在这一行上面添加了一个

<span class="entry-cjk-character-count">{{gh-count-cjk-characters value}}</span>

然后又仿照 gh-count-words.js, 写了一个 gh-count-cjk-characters.js

import Ember from 'ember';
import counter from 'ghost/utils/cjk-character-count';

const {Helper} = Ember;

export default Helper.helper(function (params) {
    if (!params || !params.length) {
        return;
    }

    let markdown = params[0] || '';

    if (/^\s*$/.test(markdown)) {
        return '';
    }

    let count = counter(markdown);

    if (count === 0) {
        return '';
    } else {
        return count + (count === 1 ? ' CJK character' : ' CJK characters');
    }
});

在这个helper中,和原来的字数统计一样,真正的统计函数 counter 是在被引用的 core/client/app/utils/cjk-character-count.js 内实现的。这个实现倒是比原来的字数统计简单不少,因为原来的字数统计还要过滤掉一些特殊字符,而中日韩字符统计只需要一个正则表达式提取所有的中日韩字符就搞定了

// jscs: disable
/* global XRegExp */

export default function (s) {
    let charCJK = new XRegExp("[\\p{Katakana}\\p{Hiragana}\\p{Han}\\p{Hangul}]", 'g');
    let cjk = s.match(charCJK);
    return cjk === null ? 0 : cjk.length;
}

效果

虽然我只是简单地在模板里添加了一行,但得益于后台的 css,我所添加的中日韩字符统计被很好地安排在了原有字数统计的旁边。

由于响应式支持,在移动端的效果也非常好。

发布

我本来想以 Pull Request 的形式让官方接受这个补丁,但是在我所发起的 PR #6391 中,其开发者表示他们正在做全新的编辑器,所以现在合并似乎意义不大。因此,作为一个临时的解决方案,我 fork 了一份 Ghost,并修改了 travis.yml, 用 Travis CI 自动打包了一份修改后的 Ghost, 并在我的 GitHub Releases 上发布: https://wasu.pw/7bWE

目前最新的版本是在1月25日的主分支基础上制作的,实际上和0.7.5差距不大,却多了一些bug的修复。在 Ghost 完成新的编辑器并加入CJK字符统计支持之前,我将持续维护这个分支,合并来自官方的最新更新并打包发布。

这就是爱

近日寒假,闲坐在家,无心作业,钉宫病发,补番心切。想到我从前已经补完钉宫四萌中的『灼眼的夏娜』和『龙与虎』,欣然决定继续补四萌,鼓足干劲,力争上游,多快好省早日达成患上钉宫病晚期的宏伟目标。于是我选择了『零之使魔』,因为它虽然有四季,每季却只有12集。

本以为只是个日常番,打算边写代码边慢慢补,结果我却发现完全不是这样。我竟然以一天一季的速度补完了这部番……因为,它让我想到了很多。

战争与人性

我想这大概是 零之使魔 贯穿全集的主题之一了。

从第二季开始,这个番就开始渲染一个观点,就是 反战。作者演绎了两种对立的观点,一种是为了名誉而不惜生命参与战争甚至发动战争,另一种是尽力避免战争的出现,尽力避免伤亡。

贵族们所秉持的正是前一种观点。也正是因为这样,当学校被用作战争的训练基地时,格鲁贝尔老师的情绪才会如此激烈; 当露易丝决定面对战争时,才人和她的姐姐们才会如此反对。戏剧性的是,作为一个害死了全村人的魔法师的格鲁贝尔老师,他的态度竟然发生了如此大的转变,从战争的狂热分子变成一个积极的反战分子,这是为什么呢?

记得不久前,中日的钓鱼岛争端再次恶化时,我也曾在微博上发表过这样一种观点,就是无论历史上谁对谁做了什么,现在的我们都不应当去鼓动战争,而是要尽力避免战争。发表了没多久,我就被一帮小粉红们挂出来说我是在害怕战争,说我是在帮霓虹人说话之类的。格鲁贝尔老师的经历让我明白了这些人如此狂热于战争的原因——他们根本就不知道,战争是什么,战争之于人们是何等存在。他们满脑子都是一种小孩子过家家的思维,即你碰我一下,我就要还你一巴掌,却殊不知这完全是两种东西。你打了别人一巴掌,你可以赔礼,你可以道歉,你可以化解你与被你打了一巴掌的人之间的隔阂; 你发动了一场战争,你虽然也可以向敌国道歉,虽然可以承认历史,承认错误,你却永远不能挽回那些在战争中逝去的生命。那些生命是不分敌我的,因为凡是参与战争的国家都不可避免地要经历巨大的伤亡,甚至是平民的伤亡。

人性有时候是很可怕的,因为它难以用长远的眼光作出决定,因为它容易冲动,它容易陷入小孩子过家家的思维,它总是争强好胜。盲目地发动战争,到最后,它又能给双方带来些什么?一句道歉?一句道歉能和在战争中逝去的万千生命划上等号吗?当年的所谓 东亚共荣 就真的值得拆散无数家庭,通过洗脑的方式来达成?同样地,今天的一个小岛屿难道就值得牺牲那些年轻的生命还换取?在战争之前,你永远也不可能料到有多少家庭会因此而破碎,有多少生灵在此之中涂炭。只要双方还能够坐下来说话,战争就是一种应当被竭力避免的东西。我曾经如此担心过,现在的高科技武器如此发达,杀伤力如此之高,如果某一天爆发了比一战二战更大的世界性战争,地球会不会因此而毁灭呢?哪怕就是那些核武器的一部分被引爆,地球也会变成不毛之地……

『罗辑思维』中有说过,当无数年后,外星的生命踏上地球这片废墟时,我们给他们留下的印象是什么?是让他们大笑一番后说,这帮人死得活该?还是让他们为地球曾经的文明赞叹一番后,不禁惋惜人类所遭遇的不幸?如果是一片核废墟的话,那甚至连大笑都不会有,因为它什么也不会给我们留下。到了那个时候,你难道还想说,我们死得值,因为我们保卫了国家的名誉?省省吧,谁还管你是什么国家,谁还管你什么名誉不名誉的,那就只是一片文明的废墟而已。

任何一个组织、国家都不对任何人的生命享有所有权,任何人的生命只归其本身所有。战争的发动者们从来就没有考虑过万千百姓的生命将何去何从。对自己生命都不负责的人,也永远不可能珍视其他人的生命。珍视生命从来就不是胆小的体现,因为人类不是为战争而生,我们有更多更重要的事情要去完成。也许,战争的狂热分子们,只是像无能王一样,内心有太多的创伤需要通过自慰来治疗罢了。

There was never a good war, or a bad peace.

-- Benjamin Franklin

信仰与信任

另外一个无法忽略的主题,就是信仰。这个主题是从所谓 异端检测 开始的。有点类似于中世纪的欧洲, 零之使魔 中也存在 信仰异端 一说,甚至可以以压制异端邪说为借口焚毁一整个村子。还有蛮人与精灵的冲突……这一切都是因为信仰。

这部番在不断地暗示信仰自由的重要。从安丽埃塔女王到教皇,没有一个人排斥半人半精灵的存在,也没有进行过任何一次合法的异端检测。甚至可以这么说,在这些人物的心中,实际上已经萌生了信仰自由的理念,只是他们未曾说出口。

信仰的冲突,在这部番中,造成了许多不必要的危害。六千年前那几乎毁灭世界的战争,人与精灵的长期冲突与隔绝,都是由所谓信仰而带来的。一方的救世主被另一方看作是恶魔,结果当面对世界性危机时仍然需要这些所谓恶魔的帮助来化解危机,这简直是一个笑话。

所谓的信仰也正是这样一个笑话。当我们信仰的时候,我们是在把某一个信条无条件地当作真理,再由此来评判这个世界。然而,在很多情况下,我们评判世界,会带上对与自己信仰不同的人的种种偏见甚至仇恨。这种仇恨在极端情况下甚至会引起纳粹主义,引发战争,比如说某以I字开头的组织。可是我不知道这些人有没有回头想过,自己为之而战的所谓信仰到底是什么?你如何确定这所谓的信条就是无可否认的真理呢?如果,只是如果,这信条被证伪,你又如何弥补自己对那些你曾经因异端而仇视的人所造成的伤害呢?

信仰什么不重要,重要的是什么都不能全信。

这句话是谁说的,我忘了。是谁说的不要紧,但是这句话说出了一个非常重要的问题,就是不能存在绝对的信仰。或者说,你不能在对某个信条或某种宗教没有彻底的理解的情况下去把它当作绝对真理来信仰。

说到这里我又想起我的外婆。她信仰基督教。苏北的很多农村人都信基督教。我不是针对她老人家,我是说,这一批人,他们甚至连 圣经 上的字都认不全,甚至无法正常阅读圣经。阅读就算了,即使是听人解释,理解上也存在困难,更不用说有自己的理解了。他们对于基督教的理解,大部分只是停留在某些地区的无良教会所说的 花钱消灾 死后上天国 之类的奇怪观点上。对于基督教中的撒旦一类的存在却全然不知,对于耶稣以外的《圣经》人物也处于几乎不了解的状态。现在都21世纪了,而这一群人的存在让我不得不感到中世纪的情况仍然广泛存在,仿佛宗教改革未曾发生过……即使是中世纪的人,他们在信仰某一教派的时候,也不会对它的教义不了解到这种地步吧。

零之使魔 中,我想比信仰更重要的是信任,是朋友和恋人之间的信任。信任不同于信仰,它只是人之间的情感的体现,它不是把什么当作真理,而是基于过去某人的行为而判断其说谎/背叛的可能性较小,从而信任他。这看起来是感性,实际上是一种比信仰更为理性的行为。

零之使魔中,信仰和信任两者始终同时存在,形成了鲜明的比照,再加上它类似于中世纪的社会背景设置,这不能不让人感到它是对中世纪的信仰状态的讽刺。绝对的真理也许不存在,但人与人之间的真情永远是存在的。

爱与奉献

战争与人性,信仰与信任,都离不开它的最重要的主题,就是爱和奉献,是才人和露易丝之间的爱和他们互相的奉献。

在战争中,才人和露易丝都想代替对方承受苦难,最后却导致双方的悲伤,虽然最后这种悲伤还是被重逢的喜悦所取代。然而这样的战争,若是在现实中,便是恋人间的生离死别了。或许,每个人若是有恋人,都会和才人与露易丝一样,想要代替对方承受一切苦难。仿佛唯有这样的爱情,才是伟大的爱情。

然而我在看到这里时却为他们感到难受。难受的是这种所谓奉献带来的离别。真正的爱是什么?若只是奉献的话,那撑死了也只能叫做忠诚,那不是爱。『龙与虎』中有这么一句话

爱不是仰慕,恋人只能是能够对等的人。

这便是了。爱应当是一种双向的情感,不应该停留在奉献上。两个人之间的爱情应当能够使一方能够理解另一方,不只是想要保护另一方,而更应该是能够预料到自己的任何一个有关双方的行为会给对方带来的东西。只是想要为对方承受一切,只会让两人之间的互相理解破灭,若是此时生离死别,就是永远的悲剧结局。

真正深爱对方的两人应当能够共同承担一切,无论这承担的事物将会带来什么样的严重后果。这也是为什么在临近结尾时,被露易丝送回东京的才人会选择驾驶战机回到露易丝的身边。因为,在这个时候,他们两人之间已经建立了真实的羁绊,也就是真正的爱。

即使相隔再远,我也要努力陪在你的身旁。

这就是爱。

这也是为什么,越是那种互相之间没有忌讳成天打嘴仗的人之间,越容易产生爱情。

这就是爱。

关于故乡

今天早上,大水群里有人分享了这么一个知乎话题

移居海外的华人怎么防止自己的子女变成外黄内白香蕉人?

其实我早就在博客的 草稿 里面存了一个标题为 故乡 的文章,然而健忘的我却忘了自己当初要写些什么。看到这个话题,我算是想起来了,我要写的不是其他的,就是故乡,就是 故乡 这个词或者说这个事物本身。

我的故乡

从我的故乡开始说起吧。

小时候,我也是很喜欢故乡的。喜欢故乡的原因,自然是故乡有很多亲戚。我想这也是很多人喜欢故乡的原因。当回到故乡的时候,总是会有很多亲戚在我的身边。因为亲戚多,自然也就有许多与我同龄的小孩子,自然也就有很多共同的话题。故乡还有什么?在当时的我看来,还有的就是乡下的各种好玩的事物 —— 至少对我来说,是从来没有见过的新奇事物。

我记得有一次国庆节长假,按照惯例,我们一家要回到故乡去住那么几天。临走的时候,我故意把某些东西忘在那里。其实是无意识地 故意 忘在那里,因为我并未清晰地意识到。我们走到半路又返回,一共这么折腾了三四趟。实际上,我就是不想离开那里而已,我对那里总是有一种依恋的感觉,即使当时的我从未说过,即使我自己从未感觉到,这种依恋是真实存在的。正是这种感觉让以前的我一到放假就想着在老家的表弟,一到放假就催促父母回老家去,即使我并不知道回到老家要去做些什么,目的是什么,即使回去以后我和在家里一样无所事事,我还是想要回去。

但我真的有这种依恋吗?我也不止一次问过自己,我真的那么喜欢故乡吗?也许并不是。随着年龄不断增长,我渐渐发现自己那种 想要回去 的情感越来越淡化。就比如说这几年,我就没怎么回去。一方面,是因为学业的负担越来越重,没有那种心情每隔一段时间就回去一次; 另一方面,是我不想回去。那里有我的姨夫一家,但现在的我却不再像以前一样喜欢他们,因为我反感某些人依附权势满嘴关系而自身文化水平却低到不行以至于花8000多大洋买了个三星却只是为了装逼; 那里有我的许多亲戚,然而他们在饭桌上只会劝酒,他们斤斤计较于出不出彩礼,亲戚之间也总是有那么多的奉承话。从前的我不懂这些,才会喜欢他们; 现在,我反而有了一种想要逃离他们,逃离亲戚的势力圈子的想法。

我曾经如此依恋故乡,而现在却无法找回那种心情。常理告诉我,人应该依恋故乡,人应该依恋自己所来自的地方,但我却想要逃离。我知道没有我的故乡就没有我的存在,但我却十分厌恶故乡,十分厌恶那里的人的作风。是我在异乡呆太久了吗?还是我根本就没有这种关于故乡的认同感?

我不知道。

何为故乡

我开始对 故乡 的概念产生怀疑。

中国人都说自己是安土重迁的民族,因而依恋故乡。中国人对故乡的定义,更多的是 祖籍,即祖上来自哪里,我就来自哪里。基于这种观点,我们就自然而然地萌生出了 叶落归根乡土情结。无论故乡是脏,是乱,还是差,在很多人的眼中,它都是个美好的地方。他们 自己的故乡,因而自动屏蔽了它的所有缺点。

然而什么是 呢?爱家,爱国,真的是这种无视它的一切缺点而把它近乎神化的爱吗?如果这被爱的对象——故乡,没有任何缺点,没有任何不足之处,那也就无所谓了; 然而事实是,大部分人的故乡不仅不是没有缺点,还有更多更严重的问题,比如说闭塞的交通,落后的文化以及所谓 淳朴 的民风。如果来自这个地方的人都基于这种 乡土情结 的文化而将故乡美化为一个毫无瑕疵的桃花源般的美好世界,那么故乡的改善,故乡的进步又从何谈起呢?所有人都会被这样的幻觉所麻痹,营造出一种 这盛世,如你所愿 的假象。更有甚者,会基于此对其他人进行 道德绑架,产生一种 凡是不赞美故乡的都是叛徒 的想法,进而出现一种阿Q式的精神胜利。由此,便有了那些对在外漂泊者的嘲讽,对留学游子的指责……

如果我们被这样的故乡概念和乡土文化所束缚,那么我们将永无进步的可能。之前在知乎上看见过一个回答,说所谓 反华势力, 不是那种整天说中国不好的人,恰恰相反,是那种整天只会赞美中国,说中国哪里都好的人,所谓小粉红。仔细想想,并不难理解。过去的文革就是典型的案例。四人帮难道说过中国不好吗?没有,是 不许你说中国不好! 你不能提出当时中国的任何缺点,因而中国在那十年间几乎没有什么发展。

我想这同样适用于我们对故乡的态度。中国传统的乡土情结实际上是由忠君爱国的思想发展而来的,是对人们思想的禁锢。有了这样的想法,人们会拒绝迁徙,或者尽量避免迁徙,因此不会常常见到外面的世界。不会常常见到外面的世界,思想就会因此被封闭在一个小圈子里,进而保持一种愚昧的未被开化的状态。没有见过更好的事物,因而会接受故乡的一切。这只对统治者有好处,因为这样的没有见过世面的愚民,往往是最便于统治的。

然而现在都什么时候了?21世纪,先生们!难道我们还想被这种东西所束缚吗?

我开始觉得,其实 故乡 是什么并不重要。重要的是,我们所爱的地方,我们所生长的地方是什么。这种爱不是羁绊,而是能洞察它的一切优缺点的爱。我们越是想要批评某个地方,对某个地方的抱怨越多,我们往往就越是依赖这个地方,也越是爱这个地方。如果我们觉得自己离开了某个地方就难以生存,那我们又如何接受它拥有的任何缺点呢?想像你去某个很破败的小城旅游,吃住条件都很差,而你只需要在这里停留一个晚上。那么,我想大部分人都会这么告诉自己,”反正只要住一晚上,忍忍吧“,就过去了。很少有人会因此而专门去做投诉之类的事情。而如果这个地方是你天天要生活的地方,情况就显然不一样了。

何为故乡?故乡本身并没有什么意义。如果非要让我下一个定义,我倾向于认为,故乡是我生长的地方,而不是祖籍。

身份认同?

所以我们可以回到我在文章开头放的那个知乎问题了。

所谓 香蕉人 的概念,实际上正是我刚刚提到的道德绑架。在人口流动愈加频繁的现在,去谈论什么祖籍,又有什么实际的意义呢?特别是对于这些从小就生活在国外的华裔,你叫他们去认同一个远在地球另一端的国度,和你强迫一个没去过中国的白人孩子认同自己是中国人,有什么本质上的区别?即便通过某些手段强迫他嘴上承认了,心理上他也无法完全接受,因为毕竟他在另一种文化的环境下长大。这也是为什么在很多移民国家, 歧视 是个很严重的问题。因为这些移民后代,无法完全认同自己来自 祖籍 的身份,如果他所认同的故乡,那个养育他长大的地方也不能接受他而歧视他的话,他将是无根的浮萍,将失去人类作为生存基础的安全感。

更直接一点,要我回答那个问题的话,我会直接说:没有办法,也不需要防止,即使防止了也没有什么意义。顺其自然便是。

博客Docker化并迁移到CaaS

起因

我用自己的VPS搭建博客,算起来也有很久了。我选择用VPS,是因为和当年比较流行的虚拟主机比起来,VPS更自由。比如说,我可以自己决定使用什么 HTTP 服务软件,可以自己选择各种程序的版本,等等。这些都是从前的虚拟主机所做不到的。

然而,距离那个时代已经有不少年了。我开始用VPS的时候,虚拟主机还大行其道;现在,我已经看不见多少人使用过时的虚拟主机了。而各种所谓 云服务 渐渐兴起,比如说基于容器的 Container-as-a-Service (CaaS) 服务。当然,VPS在很多服务商眼里也算作他们的一员,但我向来是拒绝承认VPS属于 云服务 的。

VPS用了这么久,我也是有点累了。毕竟VPS是个完整的服务器环境,一旦我想迁移,就意味着我需要从头开始重新配置整个环境。而我又是这样一个折腾党,这令人很难过。另外,虽然VPS不是个新东西,但是优质的VPS价格并不会随时间推移而降低多少,这对身为学生狗的我来说也是个不小的压力。比如说,我之前所使用的 ConoHa,不算个非常好的服务,但也要60元一个月,这不算一个非常小的数目。

什么?你叫我去用 搬瓦工chāo shòu kuáng?还是省省吧,毕竟是 OpenVZ 虚拟化,这玩意撑死也就是个容器,我一直很排斥把这种东西叫做 VPS。但是想到容器,我就想到了 Docker,这个轻量的容器虚拟化工具。它可以把应用及其所依赖的环境打包执行和分发,省去了在各种不同的环境下重新配置的步骤。即使是使用自己的VPS,Docker 也能带来很多方便。

但我毕竟已经下定决心寻找一种新的主机服务,因为我懒,因为我懒得再去配置VPS了。于是,自然而然地, CaaS 服务进入了我的视线。似乎是 Docker 出现以来,这种服务才开始兴起。正是因为 Docker 拥有的便于分发和使用的特性,这些服务才能够大行其道。虽然没有和VPS一样的权限,这些服务却能提供一样的自由,因为你可以提供自己的 Dockerfile, 可以构建自己的镜像并部署,整个环境仍然由你控制。最重要的是,这些服务比大部分VPS都便宜,因为它们可以以更小的单位来分割资源。比如说我买了个1G内存的VPS,那么无论我运行多少应用,这1G内存是不会变的,收费也始终是这样。而这类服务则是对每个应用分别分配资源,比如说我的博客只需要256M的内存,那我就只给它分配这么多内存。每个应用分配独立的内存,这就保证了在应用少的情况下浪费的减少 -- 再也不会空闲500-600M内存没用了。蛤?你说有内存256M甚至更小的廉价VPS?然而那样的VPS往往本身就只是个容器,这我刚刚已经提及,还不如使用更加轻量和方便的 Docker

服务选择

既然已经决定要使用 CaaS 服务,那就到了选择服务的时候了。说到这样的服务,我想出来的有两家:DaoCloud灵雀云

但我把 灵雀云 从列表里排除了,原因只有一个:在移动浏览器上没法使用后台,连最基本的查看日志功能都没法实现。这我就完全无法忍受了,毕竟我是个高三狗 = =

DaoCloud 的后台可以在手机浏览器上完美运行,操作全部可以正常进行,这让人十分开心。而且它对每个账号提供了 2x 的免费资源,实际上跑一个小博客已经够了,因为它相当于128M内存加上10G的磁盘空间。

所以,比较之后,我选择了 DaoCloud

Dockerfile

要迁移到这类平台,必须先把应用Docker化。而将应用Docker化的方法往往是写一个名为 Dockerfile 的脚本,让它自动执行一系列命令和配置,构建某个应用的容器以便分发。

我使用的博客系统是 Ghost。当然,已经有人为它编写好了 Dockerfile 并构建好了镜像,但它并不能满足我的需求。在上一篇博客中我也提及了,我的博客需要通过 Nginx 来替换部分被和谐或在国内访问较慢的css和js到CDN的URL,并且需要将内链中的 http 全部替换成 https, 而这些写好的 Dockerfile 都没有这样的功能。另外,我需要以较新版本的 node 运行,这也是它们所做不到的。

因此我决定自己开坑写一个 Dockerfile。考虑到 ArchLinux 的软件源比较新而且比较全,所以我选择使用 ArchLinux 作为基础镜像。在 Dockerfile 中,首先需要对这个镜像进行更新,使它升级到最新版本

FROM base/archlinux:2015.06.01

# Initialize the environment
RUN pacman -Syyu --noconfirm

nodejs 这类的软件包在官方源里已经有非常新的版本,所以只需要这样安装就好了

RUN pacman -S --noconfirm base-devel nodejs npm wget unzip git

但是, Nginx 不一样。不知道为什么,在 ArchLinux 的软件源里面的 Nginx 仍然是 1.8.0 版本。所以在 Dockerfile 中我们必须从 AUR 获取最新的 PKGBUILD 并构建安装。这也是为什么刚才那行脚本里我安装了个 base-devel

在这之前我们必须创建工作目录 /usr/src/ghost

WORKDIR /usr/src/ghost

这之后执行的命令全部是在这个目录下。于是我们就可以从AUR获取并编译安装最新的 Nginx

RUN git clone https://aur.archlinux.org/psol.git && \
  chmod -R 777 /usr/src/ghost/psol && \
  cd /usr/src/ghost/psol && \
  sudo -u nobody makepkg -sci --noconfirm
RUN git clone https://aur.archlinux.org/nginx-devel.git && \
  chmod -R 777 /usr/src/ghost/nginx-devel && \
  cd /usr/src/ghost/nginx-devel && \
  sudo -u nobody makepkg -sci --noconfirm

之所以要 sudo -u nobody, 是因为 makepkg 从某个版本开始不再支持以 root 身份运行,如果一定要运行则会中途出错。但是,如果仅仅这么做,把它切换到 nobody 用户,也是会出问题的,因为在安装时 makepkg 必须调用 sudo, 这会导致报错。所以,我加了这样一句

RUN echo 'nobody ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers

nobody 用户具有免密码的 sudo 权限。当然,这么做可能有些安全隐患,但毕竟这只是个容器,大可不必太过担心。你若是实在放心不下,完全可以在 Dockerfile 的末尾直接移除掉 sudo 这个程序。

接下来,我们就可以直接获取 Ghost 并执行安装了。在写这篇博客的时候, Ghost 的最新版本是 0.7.5

# Populate basic Ghost environment
RUN  wget https://ghost.org/zip/ghost-0.7.5.zip && \
  unzip ghost-0.7.5.zip && \
  sed -i 's/preinstall/hhh/g' package.json && \
  npm install --production

之所以有一句 sed, 是因为 Ghost 在安装时会检测 nodejs 版本。我通过 sedpreinstall 改成瞎写的字符串,这就阻止了它的版本检测,以保证顺利安装。

然后,我们需要拷贝一份配置好的 nginx.confNginx 的配置目录下。这是我配置好的反向代理

user       nobody nobody;  ## Default: nobody
worker_processes  5;  ## Default: 1
worker_rlimit_nofile 8192;

events {
  worker_connections  4096;  ## Default: 1024
}

http {
  include       mime.types;
  default_type application/octet-stream;

  server {
    listen 80;

    location / {
      #https filtering
      sub_filter 'ajax.googleapis.co[hide].[/hide]m' 'ajax.css.network';
      sub_filter 'fonts.googleapis.co[hide].[/hide]m' 'fonts.css.network';
      sub_filter 'www.gravatar.co[hide].[/hide]m' 'gravatar.css.network';
      sub_filter 'cdnjs.cloudflare.co[hide].[/hide]m/ajax/libs' 'cdn.css.net/libs';
      sub_filter_once off;
      sub_filter_types text/css text/html;

      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;
      proxy_set_header Accept-Encoding "";
      proxy_pass http://127.0.0.1:2368/;
    }
  }
}

这个配置文件是一个运行在80端口的反向代理,在代理的同时将页面内的资源链接替换成了由 css.network 提供的CDN。在 Dockerfile 中拷贝即可

COPY nginx.conf /etc/nginx/

实际上,整个配置过程到这里就可以结束了。但是,还有一些问题需要处理。一个问题是,Ghost 的博客URL是在 config.js 内设置的,而 Docker 内部的环境在启动容器时不能任意修改,即使要修改也很不方便,且不被 DaoCloud 支持。所以,我们需要一份自己修改的 config.js, 将其中 url: 'http://my-ghost-blog.com 替换为

url: 'http://' + process.env.GHOST_SITE_URL,

这样,我们只需要在启动容器的时候将环境变量 GHOST_SITE_URL 设置为博客的域名即可。

Ghost 启动时仍然会检测 nodejs 版本,所以你需要写一个启动脚本,将环境变量 GHOST_NODE_VERSION_CHECK 设为 false, 再并行启动 nginxnpm start --production。还有几个问题,比如说 DaoCloudVolume 共享的处理(其实就是建立软链接把挂载的目录里的子目录链接出来并初始化作为数据目录)和 https 的处理,我不再赘述,大体就是设置对应的环境变量并以脚本来完成对应的功能。大家可以直接移步我写好的 Dockerfile:

docker-nginx-ghost

部署

首先把写好的 Dockerfile 作为版本库提交到 GitHub。然后,我们必须在 DaoCloud 上构建出这个镜像,才能使用。前往 DaoCloud 后台的 代码构建,选择好镜像名,连接到GitHub,然后同步并构建即可。构建的过程中千万不要着急,因为这东西很神奇,构建过程倒不慢,但最后构建完了 push 的时候简直慢成doge,我还体验过传了十分钟才传完的感觉。所以,千万别着急,实在不行中断掉重新开始,千万不要狂戳。。

代码构建好以后,你需要先创建一个 Volume 即数据卷以保存数据。我就建立了一个标准的10G的数据卷(然而这对于我博客而言似乎还是大了点)

接下来就可以部署应用了。假设你使用了我的构建配置。在部署的时候,选中刚刚构建好的镜像即可。为了持久保存数据,我们还必须挂载存储卷。以我为例,首先在数据卷内建立一个名为 content 的目录,然后在部署应用时将该数据卷绑定到 /mnt/volume,再将环境变量 GHOST_SITE_DATA 设为 /mnt/volume/content 即可。之所以不使用整个数据卷而只用它的一个目录,是因为数据卷对于大部分博客而言都太大了,只用它的一个目录的话,就可以在多个应用之间共享数据卷,达到最大化利用的目的,这也是为什么我GitHub上那个 Dockerfile 里面弄了这么一大堆环境变量配置。。

当然,别忘记把 GHOST_SITE_URL 设为博客的域名(如果你不知道,可以等部署完成生成了应用域名以后再设置)。设置完成后,按下部署,再等一分钟左右,这个应用就部署完毕了。然后你就可以用它分配的域名访问你搭建好的博客了。记得先访问 Ghost 的后台。

数据迁移和域名绑定

由于 DaoCloudVolume 提供了很好的在线管理器,所以要导入旧的数据很简单,只要把原来的 Ghostcontent 目录的内容打包上传到 Volume 内刚刚创建的那个 content 目录里再解压即可。导入完成后需要重新启动一下容器。

DaoCloud 同时支持域名绑定,只要添加CNAME记录即可,未备案域名会从国外中转回来,但是不支持 https。所以我选择加上一层 CloudFlare, 用 CloudFlare 来提供SSL支持,虽然安全上还是存在问题,但至少比没有要好一些。根据客服的回复,DaoCloud 正在开发SSL支持,所以我们只要坐等就好啦。

小结

话说128M内存的容器果然还是不够用,在进入 Ghost 后台时直接卡死,所以我还是买了28元每月的收费服务,建立了256M内存的容器。

其他基本没什么大问题,用起来就和部署在自己的VPS上一样舒服。当然,DaoCloud 也存在很多不足,比如说有时候操作会莫名卡住。但它总体上还是一个比较优秀的 CaaS 服务。更重要的是有了 Docker, 一切都变得方便起来,包括升级 Ghost 也不再是个蛋疼的活。

Hello, Ghost!

Bye, Jekyll

在发布这篇文章之前,我一直在使用 Jekyll 作为我的博客工具。以前我看中 Jekyll, 是因为它是一个静态的博客工具,生成静态的页面。相比臃肿的 WordPress,静态的 Jekyll 更方便部署,随便在哪里丢上那几个生成的页面就可以部署一个能用的博客。由于省去了动态页面的执行时间,它的访问速度也有一定的提升。但是,当我用了一段时间以后,我发现 Jekyll 存在如下的问题,这些问题导致我不得不放弃 Jekyll 这一博客工具

  1. 更新、同步麻烦,需要自己写一个 GitHub Webhook 来同步自己发布到 Git Repo 内的文章
  2. 功能缺失,甚至图片尺寸调节这类非常基本的功能也要通过插件实现
  3. 博客撰写体验 不好kēng diē,特别是在移动平台上,很难找到像样的Markdown编辑器
  4. 启动速度慢导致生成时间较长
  5. 博客标签系统不灵活,难以实现多标签以及标签云等功能

第三点尤为致命,因为我经常在手机上编写博客,如果不能找到一个像样的编辑器,我就没法找到写博客的热情,而博客的更新也进入了有生之年系列。

Hello, Ghost

正巧前两天我更换VPS,从 老鹰主机 回到 ConoHa,正打算把博客迁移过去,转念一想既然自己已经受不了 Jekyll, 何不趁此机会把博客系统也换掉呢?

想起来之前去过HJCtǔ háo的博客 hjc.im,他使用的就是一个名叫 Ghost 的博客系统。于是我就去它的官网 ghost.org 看了一下。只看到官网首页的那几个截图,我就动心了,因为截图上显示的博客后台中的编辑器能完美地在手机等移动端使用。这正是我所需要的东西。

而且这又是个动态博客系统,我也就不再需要配置麻烦的 Webhook 了。相比于 WordPress, 它又轻量很多,没有一大坨复杂的只有大型CMS才会用到的功能。Wikipedia上是这么介绍的

The idea for the Ghost platform was first written about at the start of November 2012 in a blog post by project founder John O'Nolan, the former deputy lead for the WordPress User Interface team, after becoming frustrated with the complexity of using WordPress as a blog rather than a content management system.

再加上它一样采用优雅的 Markdown 来写作,我怎么可能抵挡住这样一个东西的诱惑呢。于是,我欣然决定从 Jekyll 迁移到 Ghost

配置

Ghost 配置起来并不比其他 Node.js 程序复杂。只要下载程序包,执行 npm install,再 npm start,它就会在本地监听一个端口。然后只需要用 Nginx 反向代理一下即可。

要注意的是 Ghost 并不支持所有 node.js 版本。它仅支持 LTS 版的node。而在 ArchLinux 官方源中,这是不可能的,所以我选择从 AUR 中安装 nvm 进行版本管理。我通过 nvm 安装了 Node.js v4.2.4

然后只需配置 Systemd 开机启动服务即可。我写了一个服务配置

[Unit]
Description=Ghost blog service
After=network.target
[Service]
Restart=always
Type=simple
User=peter
Environment=PATH=/usr/bin:/home/peter/.nvm/versions/node/v4.2.4/bin
ExecStart=/home/peter/.nvm/versions/node/v4.2.4/bin/npm start --production
WorkingDirectory=/home/peter/web/ghost
[Install]
WantedBy=multi-user.target 

其中所有 peter 都是指我的用户名,而 /home/peter/.nvmnvm 默认的安装路径,我指向了它安装的 Node.js v4.2.4WorkingDirectory 指向的是 Ghost 程序的所在目录。

转移文章

博客的关键就是文章,转移博客的关键也是转移文章。其他的什么都不重要,最关键的就是要把文章毫无损坏地转移到新的博客系统。

我在Google上搜索 migrate from Jekyll to Ghost, 找到了这样一篇文章

Migrating Jekyll to Ghost

这篇文章介绍了一个能够把 Jekyll 的文章转换为 Ghost 的格式的 node.js 小程序 nodejs-jekyll-to-ghost

按照这个小程序的说明,我着手进行文章的转换。但这个小程序有一个问题,就是无法处理 Jekyll_posts 目录下有多个子目录作为分类的情况。于是我只能采取最原始的办法,就是对各个子目录分别执行这个脚本,分次导入 Ghost

由于 Ghost 版本的更新, README 中所指示的导入数据的页面已经不再有效,这个功能已经被转移到 Ghost 后台的 Labs 选项内。

分次导入后,文章就原样进入了 Ghost 博客系统

标签和图片

导入文章虽然对文章本身没有任何影响,但是却丢失了标签和图片,这也是正常的情况,毕竟是截然不同的博客系统。

由于原来我使用的 Jekyll 缺乏多标签支持,所以我的整个博客只有两个标签,这反倒无形中方便了我的迁移工作。我只是打开每一篇文章,看一看内容,然后重新为它加上属于它的标签,就这么搞定了。

我本来以为转移图片会是个蛋疼的工作,但实际上并非如此。Ghost 的编辑器对图片的上传提供了完美的支持,如果一篇文章中存在无效的图片引用,那么在预览模式下它就会显示这样一个框框

只消轻触,即可上传图片。于是我只在网页上就完成了这个工作——打开原有的文章,一张张重新上传其中的图片。

主题

Jekyll 的时候,我使用的是自己适配到 Jekyll 的MDL官方示例中的博客模板。那个模板毕竟只是个示例,使用下来有一些比较难以忍受的问题。而且我也懒得重新适配一次模板,所以我选择重新寻找模板。

Ghost Marketplace 上,我找到了一个名叫sticko的主题,第一眼我就爱上了她。这个配色,这个布局,这个……简直sexy!

但这个主题已经有很长时间没更新过了,连最新版 Ghost 的导航菜单功能都没有支持。所以,我 Fork 了一份这个主题,魔改了一下

  • 添加导航菜单支持
  • 引用 Prism.js 作为代码高亮实现
  • 采用 Disqus 作为评论系统
  • 修复css字体引用的 https 支持

目前就改了这么多,已经在我的博客上使用。我非常喜欢这样的主题,所以应该也会持续维护我的那个fork。

HTTPS 和 CDN

Ghostconfig.js 里面,可以配置博客的默认URL。但是这里有个坑,就是如果你把这个URL配置成 HTTPS 开头,那么你将会陷入重定向循环。但是,如果你把这个URL配置成 HTTP 开头,那么页面中内嵌的RSS订阅链接等将无法自动改为 HTTPS。为了解决这个问题,我将默认URL配置为 HTTP, 再祭出 Nginxhttp_sub 大法

sub_filter 'http[hide].[/hide]://typeblog.net' 'https://typeblog.net';
sub_filter_types text/css text/html;
sub_filter_once off;

其中 typeblog.net 是我博客的域名。这是把页面中所有 http 链接到本博客的目的地址全部改成 https, 包括rss订阅等。这样会带来一个问题,就是如果文章中出现且一定要出现 http[hide].[/hide]://typeblog.net 之类的字样,将无法正确显示。这个问题可以通过JavaScript解决,详情请见本博客页面源码底部。

既然用到了 http_sub,那我们也可以顺手解决一下 CDN 问题了。由于众所周知的原因,主题所引用的 Google CDN 上的字体和css等无法被正确加载,且 cdnjs 在国内速度也不够理想。所以,我们可以把它替换为由兽兽RICH css.network 提供的CDN

sub_filter 'ajax.googleapis.co[hide].[/hide]m' 'ajax.css.network';
sub_filter 'fonts.googleapis.co[hide].[/hide]m' 'fonts.css.network';
sub_filter 'www.gravatar.co[hide].[/hide]m' 'gravatar.css.network';
sub_filter 'cdnjs.cloudflare.co[hide].[/hide]m/ajax/libs' 'cdn.css.net/libs';

注意,多条 sub_filter 语句需要 Nginx 1.9.x 才能支持。在 ArchLinux AUR 中有最新版本的 PKGBUILD

重定向

更换了博客系统,URL也发生了变化。Jekyll 的URL格式是

scheme://domain.com/category/year/month/date/some-article.html

Ghost 在不打开日期URL时的格式为

scheme://domain.com/some-article/

原来的链接失效,无论是对于搜索引擎收录还是对于访客来说都是不利的。好在迁移文章时并没有改变文章的短链接名称。所以使用 Nginx 的重写规则,我将老的URL重定向至新的

rewrite ^/(life|tech)/.*/.*/.*/(.*?).html(|/) https://typeblog.net/$2/ permanent;

其中 life|tech 是我原有的两个分类名称,typeblog.net 是我的域名。

大功告成

至此,我的博客已经迁移至 Ghost 并恢复正常运行。有了 Ghost 的优雅,我重新找回了写博客的热情。我不需要再忍受糟糕的编辑体验,不需要再在更新文章之后焦急等待重新生成。这是我的新博客。

Ghost 作为一个轻量级的博客系统,其开发者非常积极,十分注重细节体验(似乎领导开发者是个妹子,逃)。官方博客还时常发布一些关于如何写好博客的文章。我也相信有了这些,我的博客也会有个全新的开端。

Links

在互联网上,我不是孤单一个人。

友情链接

排名不分先后

感谢

About

诶,这家伙是谁?

我是 PeterCxy,曾用网名 颠倒的阿卡林型次元喝雪碧的虾

好吧,你是干啥的?

本人纯属折腾党一枚。小学的时候舅舅带我入了 计算机 这个 大坑,从此无法爬出。一开始是2-3天重装一次系统,后来在四/五年级的时候 ~~入了易语言这个坑, 并在坑里面折腾好久~~。初中的时候不再搞编程,而是自从初二买了一个Android手机以后开始折腾ROM移植。初三的时候知道 Android竟然是开源的,于是又学了几天 Java 入坑ROM开发。高中以后,因为编译ROM太耗时,所以又入了 Android App开发 这个坑……当然,包括开发 Xposed模块。在这时加入了 纸飞机开发团队,大概这坑我是再也填不上了……

哦对了,关于 做过主机商 的黑历史 ~~我就不说了~~

另外,现在我还是一个 FOSS(Free & Open Source Software) 追随者.

关于这个博客有什么梗么?

这个域名最初是我在 ResellerClub 瞎搜索域名的时候搜索到的,当时想找个好域名,想到 type输入 的意思,于是就口胡了这个域名出来。很可惜,typeblog.com 被抢注了,抢注者要价 $50, 但是很高兴,typeblog.net 还可用,于是这个域名就被注册了。

域名注册后一段时间内没做什么,后来我 ~~做主机商就把这域名用来做个类似于官方博客的东西~~,一开始是用 WordPress 做的。因为后来入了 Android开发 坑,就改成了一个开发博客,~~并且改用 jekyll 做了静态博客。目前该博客丢在自己的VPS上,使用 GitHub Webhook 来实时更新。~~ ~~改用 Ghost 作为博客系统,自己找了套免费模板魔改了一下。~~ 后来自己写了一个博客引擎 Typeblog(本博客配置文件及文章位于 https://github.com/PeterCxy/typeblog.net)。

我有话对你说/我想交换友链

在下面留言就可以了, 广告会被删除

交换友链的站点类型必须是博客,原创内容占90%或以上,并且 必须全站强制HTTPS

版权声明

除非特殊说明,原创文章采用 CC Attribution-ShareAlike 4.0 International 许可协议进行许可。然而,如果作品在中国大陆地区使用,则须采用 CC Attribution-ShareAlike 3.0 China Mainland 许可协议。

主题

本博客目前使用的主题为移植自 WordPress 主题 Diasporatypeblog-diaspora

关于评论

本博客曾经使用 Disqus 作为评论系统,现在已经迁移到 ISSO (一个自搭建的 Disqus 开源替代)。

PGP

公钥指纹: 708D FB13 CBED 9AB3 9449 DBFB 41C0 4120 C297 B594

Keybase: https://keybase.io/petercxy

本博客所有权证明: https://typeblog.net/keybase.txt

导入方式:

gpg --fetch-keys https://keybase.io/petercxy/key.asc
gpg --keyserver hkp://pgp.mit.edu --recv-key 708DFB13CBED9AB39449DBFB41C04120C297B594 

执行第二行代码时,若提示 unchanged,则表明你已经成功导入我的公钥,没有被篡改

新浪微博身份

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

I hereby claim that

1. I am @Mogician_Reion on 新浪微博
2. I am PeterCxy on keybase.io
3. I have a PGP key whose fingerprint is 708D FB13 CBED 9AB3 9449 DBFB 41C0 4120 C297 B594
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEmleE0pKdoIT2AVoIcfX7Tk8/1U8FAlh+qCwACgkQcfX7Tk8/
1U/7KRAAgMQywAYxZFLxlR8SflvGClYPSpDKYSCIJlW1d16TstWwaWTxz1i65Sed
LIMFihnjuIaYZ4P5nxoXhGh1GO7JcZAXjMbhXNkulmNbbixb4OPmAafxVIXrbLUY
j9kMbW+vSWwKBjthmdFqccFEReidzwJy6y3mdgi12K0fgUc670sCZMLZxnEmXe0v
EIir6chyLciPxKFJHb1jBBAIz7DI0Na6WJYlIVLPj8tfNHlk7wrfZ8dSGgWy6h+7
ThVq+TBGukwr6nCpKldWpVwvMYE/p3Ij/pmCNf4/tD3BMk6XkD5A7fvw5Lhf84vq
DC1G3ynSiEyoyKokjjO52lAG+rXor9yNVXiS1n9sw6UtnH2jTxKFtw3C6JFTkHo+
33yaLT710TwZez8+0lbNrAFQnlvJAYvbuTHJVdjotiDs/bENzj/82W24zf972xKL
4mSID02ZUN04bGW7PmJAX8f5QfnkFSq6Ci5Y/J941S34iY/iz4Td76YOsjgkM7Dv
VJ3mhMeeEptIG1DYjdM3SDCa55cAs+wXpEu79NkfQluSNa4PvpZFgQ0xDcFE9Oca
GoJQyIA+SLWoLikvF0wGMywcrM2re4ZRbM7i8nielRsVdlbfdH0Wns8PG+75thkt
6zHx0yliTjPpnbM5du1jnPvwjJ2TcxMHEOlJ3heN/NH6CnyPUG8=
=abkH
-----END PGP SIGNATURE-----

Telegram 身份

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256


I hereby claim that:

1. I am @PeterCxy on Telegram
2. I am PeterCxy on keybase.io
3. I have a PGP key whose fingerprint is 708D FB13 CBED 9AB3 9449 DBFB 41C0 4120 C297 B594
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCAAdFiEEmleE0pKdoIT2AVoIcfX7Tk8/1U8FAlh+qHEACgkQcfX7Tk8/
1U/+zg//bC6WDIPFqXw8HuaSvLTX7Vpm1bBcPhJaQjS+EUTvhqjVL95rCO93+mkn
lX2PEnd3ccZpXRhNiNRqPl11DNRdfRL/VLtzfXh8r4tZBjbKcNHU+WySZV0r+EoA
OXOkHazzkUiAVxzcAfCJ9VRVTmExZOoEjfscFLwa90QeWRiRZSqUrFahIAQNykfj
eZXInjKb0O57ENsYpgCsm/+q/BZPM4cEQpE8MNumfzV+8rrcbDSz1QW3Qr2a+VsO
Lu+vQxypBkgv9J2aWuMqV7o+ZdlqmLLCpBWAL4k1uSGzKGwy63GUAsxTYWd97sGK
c1W2bmA1m4sr5LuR78UnmI/q86aPjnXVSMxdaKqfBbTIaT7atnlAVAvbqv8JigUI
oipQ8KCp7Pl516iiw0/9A2mMThIbJDuPPl4zInZEHYsr6MDdEgIR/6jx+JERhjWG
CZbE0+wHicVwuPV0RASjiT9xL0n1y+GZ7iuvJjSJfiAgeE5T+vCduQuUO42jjLHM
ix3xg0phkhwAViUMk4bjGW+ixRZbxzSUUee8hwA8/HdJcETbY6YqoPiq4szi05cc
60m12QrupSGmA2Ski58tGSi43MwfnchojqzaeWsnEBrGEIGc/zbVhJpPFfz1WhDG
mpFrYdvwciubeih8XiBRuyv2chVqT6ndobyg6FesbKZW3A4sK9w=
=TmIq
-----END PGP SIGNATURE-----

さようなら、2015

年年岁岁花相似,岁岁年年人不同。

又到了这样的一个时候,这样一个辞旧迎新的时候。说实话,这个时刻和以前任何一个时刻并没有什么区别,不过是一个普通的时间点,过了这个时刻,什么也不会发生。只是因为蓝星人们把它作为 年龄增长的标志, 这一刻才有了意义。

昨天睡觉的时候,突然想到自己应该写一篇博客把过去的这一年记下来。我从来没有写这种年终总结文章的习惯,但是这一次我却觉得必须写。或许是我做作,但是2015的确是值得记录的一年。

2015年的这个时候,我正在准备 小高考, 和我在当时刚刚结识几个月的新同学们一起。在高一的时候,我身处一个有些死气沉沉的班级。也许是因为学霸太多,也许是因为我自己的情绪,或者是因为班主任,高一的时候我觉得那个班级充满了压抑。下课,除了闷头写作业的,便是在睡觉的;甚至连说话的声音都没有,若不是窗外有其他班级的声音,根本不可能区分上课下课。那时候班主任是个语文老师,在语文课上只有一个要求,就是记笔记,凡是他在屏幕上打出来的东西,都要如实记录,于是语文课在大部分情况下就是抄写课,沉闷得可怕。那时候我一度萌生做个 倒计时 装置的想法。而在高二(高二文理分科重新分了班)的新班级里,即使是在 小高考 这样一场考试面前,也是该说的说,该笑的笑。哦对了,我才不会告诉你们,那时候四门非主课复习,老师人手不够,把两个班并在一起上课,因而打乱了座位,我也因此坐在女神的前面(逃)。现在想起来,那是离女神最近的一段日子。另外,就在前一个月,在反复被强调重要性的高三 零模 之前,我们还在为元旦晚会上的节目排练,整个班的气氛都因此嗨了起来。现在回顾在这个班级里的经历,不禁暗自庆幸在2014年作出了正确的选择,有幸分到这样一个班级里。这让我重新拾起对高中生活的希望,甚至对它产生了不舍的感情。真的,我不想离开这样一个班级,因为离开了可能就再也没有这样的人品了。但所有好的事情都有结束的一天,我也只能选择和这样的同学一起像以前一样地度过这最后的六个月。

同样值得铭记的是2015年的暑假。暑假有很多,但这是第一个属于我自己的暑假。暑假之前,父母告诉我,暑假里我们有个全家出行的计划。我立刻就想起之前房天语房老板和我提起过的开发者会,也就是后来定名的 Connext 一事。当天晚上我就联系房老板,房老板也立即给我安排了演(an)讲(li)的时间。其他人见网友,会担心自己的安全;而我在北京,却真正认识到了我所在的这个互联网圈子是个什么样的圈子。无论认识不认识,是演讲者还是听众,所有人都是一样的友(dou)善(bi),一拍即合,没有什么尔虞我诈。作为一个第一次演讲的人,我让话筒都发生了肉眼可见的抖动,但是没有一个人有不耐心的声音。在经过北京的只有一半有无障碍通道的坑爹神设计过街天桥时,同行的人主动把我搬下了台阶。我之前也写过一个博客,说那是一个无尽的八月。我宁愿它永远都没有结束,就像凉宫春日的暑假一样,因为它留下了太多难忘的回忆。

过去的一年里,我在 GitHub 上共提交了1393条提交,给我的项目推送了很多更新,也曾被新浪钦点,却因此在微博上获得了数千粉丝。我两次搞坏了 ArchLinux, 两次搞坏了 Gentoo, 还弄坏了一个米2。唔,真是难忘的一年呢……

哭过、笑过、恋过、恨过, 这大概就是对我的2015的最好的总结。不管怎么说,2015是过去了。不得不感叹的是 岁月不饶人 -- 我是1998年出生的,掐指一算,2016 - 1998 = 18,已经在这世界上度过了近18个春秋。在将来的一年里我会迎来真正的成人礼,也就是十八岁的生日,而我准备好了吗?

可能没有,又或者早就准备好了。我想大部分人在这时候都是这种感觉。过去是难以忘记的18年,未来却是无穷的迷茫。在内蒙古的草原上的时候,我正有这种感觉,仿佛一切都在无穷远处,天苍苍,野茫茫,不见归宿。但是有一点能确定,就是 仍然是 。有人说经历会使人变得越来越圆滑,但我想,我将避免在这个过程中丢失自我。无论满没满18岁,我都将是我,我都将是那个不妥协不将就不屈服的我。我的原则是不能违背的。我对技术的坚持是不会被放弃的,即使我本来就没什么技术,即使我已经不适合去做什么技术,我也不会妥协。我不会妥协于任何所谓的权威,我将用自己的眼睛和心智去了解这个世界。这是我在2015年总结出的我的信条,我将在这里提醒成年以后的我,提醒今后无论何时的我,不管发生了什么,都不能忘记了这些。忘记了这些,我将不成其为我,我将失去我存在的最基本的意义。即使我的一生会庸庸碌碌地度过,我至少能告诉自己,我真正地存在过,这是真真正正属于自己的人生。《Angel Beats》的世界并不存在,但是如果存在,我想当我误入其中之时,我可以像音无结弦一样对自己的一生感到释然。

数着、等着、望着、怕着

青春已时日无多

如果把青春界定为18岁或者20岁,那么2016的到来意味着我的青春也将时日无多。但它不会是一梦蹉跎。就像2015一样,这新的一年也必然有其难以忘怀之处。新年的钟声已经敲响,没有更多的时间沉湎于过去的一年,不如就大声地说

さようなら、2015

こんにちは、2016

Hello, Marshmallow!

和我的Moto X 2014一起吃的那些棉花糖。

Android 6.0 也即 Marshmallow 出来也一个多月了,而我一直没能用上。好在 Moto X 2014 还算是个旗舰机,这等待的过程并未持续很久。

CyanogenMod 13

首先给 Moto X 2014 带来棉花糖的是非官方的CM13。这个非官方的CM13是由 xda-developers 上的一位开发者自行适配的:

http://forum.xda-developers.com/moto-x-2014/development/rom-cyanogenmod-13-t3243768

作为折腾党,我肯定第一时间就更新了CM13。但是最初的一个版本有严重的问题,比如说锁屏密码无法设置。这个bug后来修复了,可是 Smart Lock 的人脸解锁仍不正常。而且根据作者近两天所说

Once again, im here to sadly say that I was robbed and the robbers took my cell phone, AGAIN, and at the moment I cannot buy another phone to continue the development work. I apologize to all who encouraged my work, and who believed in me.

也就是说,作者的手机被人抢劫了。无论真实与否,这个非官方的CM13是不可能再更新了,而CM13官方夜版也还遥遥无期。

Motorola Stock

非常开心的是,仅仅过去了一个多月,摩托就开始对巴西版本的 Moto X 2014 和今年的Style机型推送官方的 Android 6.0 升级。

在XDA上,很快有巴西用户提取了这个更新并上传提供他人下载

http://forum.xda-developers.com/moto-x-2014/general/ota-android-marshmallow-6-0-final-t3248315

这个更新包是以OTA而非完整固件形式提供的,需要先刷入巴西版的5.1底包才能进行OTA。但是因为Moto的分区表和 bootloader 匹配问题,我害怕OTA会玩坏,所以选择直接使用 TWRP 还原其他用户提供的备份包。备份只包含system和boot,只要分区表是国外版本的,就没有任何问题,不用担心bootloader和分区表的版本问题。比如说,我的机器的分区表和bootloader就都是 XT1092Android 5.1 的版本。

无论你用哪种方式刷机,请一定 不要刷基带和其他通信相关分区 !!!!!! (modem, fsg等)

电信

彼得蔡日常之一就是修电信问题>_<

显然,巴西版的ROM刷完以后是不可能直接正常使用中国电信大法的。所以做完以后第一件事就是把它root了(那个ROM帖子里有root方法)

然后开机,没信号,没关系,直接激活进入系统。连网络,下载个支持root的文件管理器。

我在 build.prop 里面发现了这一行

telephony.lteOnCdmaDevice=0

把它改成1,然后把 /etc/apns-conf.xml 换成来自 CyanogenMod 的同名配置文件 https://raw.githubusercontent.com/CyanogenMod/android_vendor_cm/cm-13.0/prebuilt/common/etc/apns-conf.xml. 保存后重启,这个时候,系统应该能够识别电信这个运营商,但是无法显示信号。这时,到 设置->更多->移动网络 里面,把APN改成电信LTE,然后把网络改成3G,再等一会,应该就会出现信号。如果不能,那么你需要手动在调试界面 (拨号 *#*#4636#*#* 把网络模式改成 GSM/CDMA auto

但是就算是这样,你也只能使用3G。一旦开启4G,系统就无法保持 LTE/CDMA 这一网络配置,而会自动跳到 LTE/GSM, 这就无法使用网络了。猜想这可能和巴西本地的运营商配置有关。

于是想起了我以前做的一个 Xposed 模块,叫 LockNetworkType。这个模块就是用来解决这个问题的,它能够把手机的网络模式锁死在某一个值,通过在 RIL.class 里面加入钩子来阻止网络模式的改变。问题就在于 Xposed。好在,Android 6.0 已经有可用的 Xposed

http://forum.xda-developers.com/showthread.php?t=3034811

刷入后,安装我的这个模块

https://github.com/PeterCxy/LockNetworkType/releases/tag/1.1

在里面选中 LTE/CDMA,锁死,再到设置->移动网络里面选中4G模式即可。

至此所有影响使用的问题都已经解决

彩蛋

Marshmallow 的彩蛋还是一个类似 Flappy Bird 的游戏,不过,这次比5.x多了一个新的功能,就是支持多人模式,最多六个玩家。系统会通过识别触摸点位置来区分不同的玩家。这个彩蛋已经俨然一个完整的游戏了。

Runtime Permissions

Android 向来饱受诟病的就是权限问题,所以在 Android 6.0 里面引入了新的权限机制。所有应用在使用敏感权限之前,都必须向用户发出请求,被允许后才可以调用对应函数。

但是这需要应用代码的大幅度修改,需要显式发送请求,这相比于一些国内ROM所做的权限管理机制,还是有一些 的感觉。不管怎么说,这至少有助于限制一些开发者随心所欲的行为,也让人意识到权限不是能随便声明和调用的东西。

不过支付宝新版虽然适配了这个机制,可是如果不授权的话就会崩溃。呜呼,若是这样,也真是没救了。

其他杂项

在我眼中最值得关注的变化就是以上两点。还有一些摩托的优化、修复之类的不值得列举。在UI方面,Android 6.0 在启动器上有较大变化,在水波动画逻辑 (Ripple) 上的变化也比较大,以至于我甚至以为出了问题。以前的水波是按下后就会触发,而现在是离开触摸区域或释放后才会触发水波动画。这实际上也更加符合真实的水波。

新版本还替换了默认的SSL实现,采用自己fork出来的 BoringSSL 替换了以前的 OpenSSL (好吧我都想弄个 ExcitedSSL 了)。同时,替换了以前的DHCP实现,虽然不知道目的是啥。

开发者选项里面的模拟位置不再是一个全局开启或关闭的选择框,而是一个列表,可以单独对某些app开启模拟位置授权。这相比以前也是个进步吧。

在存储页面多了个自带的文件浏览器,虽然那个并不能用来安装apk。

暗黑UI被摩托吃掉了。

另外,也许是因为我刷了个官方ROM,真的好省电的说。。。

以上。

关于我的 Moto X 2014 (victara)

这并不是一篇评测。

逝去的米2

上个星期,所谓 MIUI7 正式对外发布。作为一贯的MIUI黑,我居然在那天想到把我的米2更新成 MIUI7 试试看。当天上MIUI论坛,看到了一个置顶帖子,是在收集情况,这个情况就是在升级了最近的MIUI版本以后,很多人的米2出现的无限重启的情况。当时我没有在意这种问题,直接略过下载了 MIUI7,想也没想就刷上了。

刷完以后,用了有三个小时吧,觉得烫手,而且仍然不甚喜欢MIUI的风格,于是又把 MIUI7 清除掉,刷了 FlyMe。但是,这个 FlyMe,开机动画每次进行到20s左右,就立刻黑屏重启。当时我认为是ROM的问题,就把原来的 CM12.1 的备份恢复了,继续使用。

然而到了晚上以后,问题出现了。在某些时候,如果关掉手机重新开机,每次进行到快要进入系统的时刻就立即黑屏重启,而进入 Recovery 做什么都没有关系。只有插上电源才可以成功开机。再后来,即使有满的电量,也完全不能离开电源。

更换了电池,并没有任何作用。看起来是某处IC烧掉了。目测和刷机或者当天试用的过程中出现的大量发热有密切的关系。

于是我的米2就这么变成了个电子垃圾。

新手机

后悔我没看那个帖子是没有用的。

米2坏掉了,总得换个新手机,而非常高兴的是粑粑居然通过了我的请求。于是我开始物色一个新的手机。

作为一个 Android 开发者,我也提到了,我不能忍受 MIUI 这种 自以为是苹果,却又学不像的风格。小米的手机虽然有两款目前有 CyanogenMod 的官方夜版支持,但是在国外的用户并不多,很有可能像 米2 一样在某一个版本以后再也收不到夜版的更新。而且基于小米官方消息的闭塞以及 万年4.4,我不可能再去换个小米的手机。而 Nexus 系列……虽然有的支持电信的网络制式,但是每次系统更新都必须想法子重新破解网络制式,这非常麻烦,而且我作为一个准高三狗在开学以后不可能还有很多时间碰电脑。

于是想到了 Moto,一个曾经是 Google 子公司的公司。他们的官方UI从来都不对 Android 原生样式作任何改动,而且国外的开发者也很活跃。Moto G? 好像某些时候还不如米2。看了 zealer 做过的 Moto X 2014 评测,又查看了 CyanogenMod 发现有官方的每夜版支持。正好碰上了官方最近打折(因为要出新款了),所以就在他们的天猫旗舰店上入了一个皮革后盖的 Moto X 2014

什么?我为什么不等新的 Moto X Style?那是因为,再过一个月,我就不一定能换手机了,毕竟还有期初考试摆在那里……

到手 & 解锁

因为天猫上的存货都在上海,从上海发顺丰到苏州,只要发出来肯定是一天到。所以我很快就收到了我订购的 Moto X 2014。我并不擅长拍所谓的开箱照,所以这里并没有任何图片。盒子打开以后,配件排列的很整齐,一层一层取出来就是了。手机上有一层保护膜,不撕掉简直不能看。这可不是某些手机品牌所谓的原装膜。

第一次开机进去,是原生风格的设置向导,只是没了Google服务 —— 这个啊,国行版本,当然没有……进去以后也是原生的启动器,只不过某些原本包含在 gapps 里面的东西,比如说短信,浏览器,被换成了不知道是什么东西的版本,这也很正常,毕竟在国内。然而有些服务的替换实在让人难以忍受,比如说百度家的定位服务……而浏览器是我最不能忍的,简直比 AOSP 里面自带的浏览器还要差,我就不上图了,怕恶心到你们……

所以第一次开机以后,马上去开发者选项里面打开了 解锁 这一项,准备开始解锁。我用的手机,能不解锁?

国行的 Moto X 是可以在摩托罗拉的国外官网上获取解锁码的,官网上有详细的教程告诉你如何解锁。我只想说一下这里的一个大坑,就是官网上教你 同时按下音量减和电源键 来进入 fastboot 模式,但我自己试了好几次都没有成功。后来在网上搜索,才发现正确的姿势是这样的:首先,同时按住 音量减电源;然后,松开 电源,但仍然按住 音量减;最后,松开 音量减。此时你的手机就成功进入 fastboot 模式了。

进入 fastboot 以后,只要一步步跟着官方的教程走,就可以解除锁了。解锁以后,每次开机的第一屏都会变成一个告诉你手机已解锁的警告,这十分烦人。在 xda-developers 上面,有破解过的 logo.bin, 在 fastboot 下刷入以后即可移除警告,并且有的 logo.bin 还带有自定义的开机 logo

刷机

解锁为了什么?当然是为了刷机啊。

国行版本的第三方ROM是非常有限的,仅有的几个ROM也大部分都是个人维护,更新周期不忍直视。不过事实上,因为硬件层面几乎相同,所以国行版本的 Moto X 2014 即型号为 XT1085 的版本是可以和国外的其他 Moto X 2014 通刷ROM的。为什么直接刷入会报错呢?那是因为国行版本分配的 system 分区的大小与其他版本的不一样。

由网上得知国行版本的预装ROM Android 5.0.2 对应的分区表的版本号为 02,只有这所谓 版本 相同的两个型号的 ROM 之间才可以通刷,应该是因为 bootloader 内写死的起始分区位置导致的。不过很幸运,欧洲的行货 XT10925.0 版本ROM的分区表版本同样是 02。所以,,我就找了一个 XT1092 的官方刷机包,刷入了其中的 gpt.bin 和整个系统。

事实上只要刷 gpt.bin 然后清除数据,这以后就可以直接刷CM的夜版。但是想到一个官方刷机包有 1.02GiB,我不至于下了个那么大的刷机包就为了里边的一个分区表吧……所以我就把系统也刷进去了,想体验一下欧行的系统。

当然,因为是国外的版本,所以所有预装app都是 gapps,看起来就和亲儿子没什么大区别。但是这样刷入的ROM是不能OTA的,因为OTA一般都带有 bootloader基带 之类的固件,使用后可能导致黑砖。

所以在体验了一会儿以后我就重启再次进入 fastboot 刷入了 TWRP Recovery 然后又刷入了 CyanogenMod 12.1 Nightly

电信

在这段刷机的时间内,因为我的电信卡是大卡,而且芯片是老式的无法手工剪成小卡,所以我一直没有插卡,也不知道具体的电信支持情况。晚上拿到了麻麻去营业厅换的小卡,于是插上测试电信的支持。

第一次插入电信卡的时候,CM12.1 一开机就有信号,但是无法上网,无法打电话。以为完全不支持,但是折腾了一会儿以后就发现原来是在设置内的 订阅类型 错误,默认是 NV,而中国电信的应该是 RUIM。所以手动选择了 RUIM,于是一切通信功能都恢复了。

但是第二天早上,我更新了一下新的 CM Nightly,信号就不见了。即使手动调整网络制式到 CDMA 或者电信的 LTE,也会很快被系统自动调回去,简直不能理解。这个时候我想起了我以前写的 Xposed 模块 LockNetworkType,但是当时并没有 4G,所以它锁定的仅仅是电信的 3G 制式。因此,我对那个模块进行了改造,添加了一个主界面来选择网络制式,并且开放源代码于

https://github.com/PeterCxy/LockNetworkType

完成以后,安装 Xposed 框架,开启模块,电信完美使用(除了在某些界面,比如说关于手机的电话信息界面,会有FC的情况)

功能

我们都知道,New Moto X 的正面有很多传感器,配合协处理器可以实现动作感应。买 Moto X 之前有人告诉我刷了 CyanogenMod 以后就没有官方的那些手势可用。但是,我发现,现在的 CyanogenMod 的设置界面中已经有一个 手势 选项,里面的功能与摩托官方ROM内的并没有任何区别,包括 chop-chop 挥手 等手势都在里面。而所谓的 Ambient Display, 也早就被 Google 加入了 AOSP 豪华午餐,在 CM 里面默认是开启的。因此,目前的 CM 在特色功能上与官方ROM并没有什么区别。

以上

给Jekyll静态博客扩展动态评论系统

其实是因为 Disqus 被我玩坏了 (大雾)。

起因

各位都知道,Jekyll 是一个静态的博客系统。它的文章以文本形式存储,当文章更新时,通过解析模板重新生成的方式来更新页面。这样的博客对服务器的压力非常小,但同时最大的弱点也在于 静态,也就是无法原生实现一个评论系统。

然而评论是一个博客非常重要的功能。所以,大部分人在使用 Jekyll 的同时,都会配合 Disqus 来实现评论系统。但是我在开头说了, Disqus 这玩意被我玩坏了,所有的头像都处于叉烧包状态……而且毕竟使用第三方的评论系统不利于博客的自主管理。

所以,在昨天(2015年8月14日)下午,我决定,给自己的博客扩展一个动态评论系统。这不是 Yet Another, 这不是 Yet Another.

后端: 构想

这个暑假我在学写 CoffeeScript,并且已经使用 CoffeeScript 编写了一个 Telegram 机器人,写了一个叫做 korubaku 的用于避免回调地狱的 nodejs 模块。所以,我的第一想法就是使用 nodejs + CoffeeScript 来做一个后端。

至于数据库嘛......数据库......就在 Google 上随便敲了个 Database,然后开始随便选,于是就选中了 Redis 作为数据库系统。

一开始,我的想法是,使用一个单独的有序集合 set 存储所有的评论并为它们赋予唯一的ID,然后对于每个允许评论的页面,单独创建一个有序集合并且按照时间顺序存储其对应的评论ID。

这是我在开坑二十分钟内产生的简单构想。然后,我就开始着手实现这个构想。

后端: 实现

上面的简单构想,实现起来是非常容易的,只不过我在使用 redis 的时候绕了好几个圈子。一开始使用的是 redis 中被标为 set 的东西,因为我并不知道 redis 中还存在有序集合和无序集合的区别。于是,这样做出来的后端,返回的评论顺序是一团糟,根本不知道哪个在先哪个在后。

然后我才在 redis 的文档上找到了 sorted set 这种东西,也就是根据一个 score 值进行排序的有序集合。这才是我所需要的东西。这个时候我所用来标记评论的 id 使用的其实是 index,也就是它在这个数组中的位置,具体的实现是像这个样子

[err, id] = yield db.zcard commentSet, ko.raw()
if err?
    id = 1
else
    id += 1

每次添加新数据,就先获取数据库内已有的评论数量并加1作为新评论的ID。这个逻辑存在一个潜在的问题,我在后面会提到。

接下来的问题是关于评论之间的回复关系。在博客评论的时候,无论是博主或者其他人,总会想对其他人的评论进行回复。我第一个想法是对每个评论用一个 reply 属性指向它所回复的评论的ID。然而,这样返回的数据,到了客户端的浏览器中,非常不便于处理,因为还需要本地解析评论之间的关系。所以不如直接在服务器返回时处理。当从数据库里面取出一条评论的时候,在把它 push 进准备返回的数组的同时,加入到一个 id -> comment 的映射里面。由于我取出的顺序是按照时间顺序,因此当一个评论的回复被取出的时候,这个评论本身一定已经在 id -> comment 的这个映射里面了。所以,只要通过这个映射找到父评论对象,在其 replies 属性中把这个回复添加进去即可。由于这里的对象是一个引用,所以在返回的数组里面的父评论对象也会即时更新。

就是像这样

response = []
map = []

for r in reply
    r = parseInt r
    [err, [cmt]] = yield db.zrangebyscore commentSet, r, r, ko.raw()
    if !err? and cmt?
        cmt = JSON.parse cmt
        delete cmt.email
        cmt.id = r
        if (!cmt.reply? or !map[cmt.reply]?)
            response.push cmt
            map[r] = cmt
        else
            orig = map[cmt.reply]
            if !orig.replies?
                orig.replies = []
            orig.replies.push cmt
    else
        console.log err

请无视有关数据库的部分。

但这样的评论逻辑还有一个问题,就是评论的嵌套问题,必须限制嵌套的层数,否则做出来是非常难看的。本来想在服务端返回的数据里面做处理,结果后来直接在前端发来的请求上做了处理……

关于头像,我本想让用户自己上传头像,但是这样似乎又要实现一个用户系统……不是很符合我的初衷。所以我使用的是 gravatar 的解决方案,在每次请求的时候,检查用户提交的邮箱(其实就是往 gravatar 发个请求检查返回值)是否有对应的头像,有的话,就使用 gravatar 的头像。由于众所周知的原因,这里我使用的是 V2EXgravatar 建立的 CDN

这样,我的最初的后端就完成了。

前端: 构想

完成了后端,经过简单的测试以后,我开始着手编写前端的逻辑。

我选用的当然是 jQuery。我所使用的博客模板,其实也就是 MDL 的博客模板,里面带有几个示例评论。所以,评论的排版不需要我过多操心。

我的后端里面少了很多逻辑,比如说日期的格式化、嵌套层数限制的处理,这些都必须在前端得以解决。

我并不想破坏 Jekyll 作为静态博客的本质,所以我的计划是在页面加载完成后触发 JavaScript 再从服务端获得 json 格式的数据进行渲染。

前端: 实现

我首先实现的是页面的渲染部分。使用 jQuery 的POST和 Jekyll 提供的页面ID,可以简单地向服务器请求本页所有的评论。请求完成后,转交回调函数将其转化成 HTML 格式并 append 到容器元素内,这没什么复杂的,只是要用评论的ID标记各个添加的元素,否则没有办法进行后续处理。

然而在实现后端的时候我并没有实现评论嵌套层数的限制。所以,在实现前端的 回复 功能的时候,我必须对此进行处理。在后端内,我们可以看见,我使用 reply 属性指向一个评论所回复的评论的ID。所以,当前端从服务器得到评论列表的时候,我使用了类似的后端的方法,把已知的所有评论存储到一个 id -> comment 的映射里面

commentCache[item.id] = item;

这样,在回复的时候,首先通过所点击的按钮的 id 属性获得用户要回复的评论ID,然后,从这个映射中找到这个评论,检查它是否是另一个评论的回复。如果是,则重定向当前回复到这个评论的父评论上,而保留一个自动生成的 Reply to XXX 文本,以标记用户回复的对象

reply_to = $(this).attr('id');
$('#form_content').trigger('focus');
$('#form_content').val('Reply to ' + commentCache[reply_to].nick + ':');

if (commentCache[reply_to].reply && commentCache[reply_to].reply > -1) {
    reply_to = commentCache[reply_to].reply;
}

我使用一个全局变量 reply_to 来记录用户需要回复的评论ID。哦对了,还需要保留用户取消评论的能力,因为手抽点错是经常发生的。这个简单,在评论内容框的内容被删除至空的时候自动删除全局变量中所记录的要回复的评论ID。

后端也没有实现日期逻辑,所以日期是在客户端实现的,格式化为字符串后直接提交到服务端,按照客户端的日期发布评论。所以,找了个简单的日期格式化函数

function formatDate(date) {
    var monthNames = [
        "Jan", "Feb", "Mar",
        "Apr", "May", "Jun", "Jul",
        "Aug", "Sep", "Oct",
        "Nov", "Dec"
    ];

    day = date.getDate();
    monthIndex = date.getMonth();
    year = date.getFullYear();

    return monthNames[monthIndex] + ' ' + day + ', ' + year;
}

大家可以发现,在服务端的时候,评论的顺序是按照提交时服务端的时间排列的,而具体显示的日期则是按照客户端的时间……所以……

当然,对于 emailnickname 两个属性,由于一个用户不会经常改变自己的邮箱和昵称,所以我使用 jQuerycookies 插件,在提交成功时保存用户的邮箱和昵称,下次加载页面直接自动填充。

在提交表单时,我注册了一个事件,直接提交到 JavaScript 里面进行检查后 POST 到服务端,然后靠浏览器的表单提交逻辑自动刷新页面。

部署

启动后端,使用 nginx 随便反代一个路径上去即可。

问题

ID

对,就是评论的ID。在上面大家看到了,我使用的是成员位置作为评论的ID。这在无并发的单线程模型下是绝对正确的,但是 nodejs 并不是一个阻塞式的框架,在同一时刻可能有好几个请求正在被处理,这就导致了仅仅靠获取当前评论数量的方法无法获得正确的ID。

所以,我修改了逻辑,使用提交时刻的时间戳记来进行区分。由于时间戳记以毫秒为单位,且因为 epoll 不可能让服务器在1毫秒同时处理两个请求,所以在这个情况下,使用时间戳记,就可以解决ID重复潜在故障。

评论提交

如果仅仅依靠表单的自动刷新,不仅不优雅,还会导致可能在提交完成前就自动刷新,使用户看不见自己的评论。所以,我使用一个容器包裹了所有的评论,取消了表单的自动刷新,在提交完成以后清空父容器重新加载,这样就不需要刷新页面,也确保了请求已完成。

Disqus 评论迁移

我并不舍得丢下我以前的评论。所以,必须进行迁移。

好在 Disqus 提供了API。通过API可以获得帖子(页面)列表,而每个页面都能获取其原始链接。由我的前端代码可以发现,我在评论上使用的ID与页面URL是直接相关的,所以通过Disqus的API返回的页面链接可以构造新的ID,并通过POST将其上传到新的评论系统。

Disqus 的评论API没有返回作者的email,导致我的 gravatar 方案无法使用,所以我改成了通过作者的用户名获取 gravatar 提供的 identicon,就是根据哈希值计算的随机头像。

我写了一个python2脚本来做评论的迁移

#!/usr/bin/env python
# Encoding: UTF-8

import httplib
import urllib
import json
from datetime import datetime

token = 'token-of-disqus'
key = 'key-of-disqus'

client = httplib.HTTPSConnection('disqus.com', 443, timeout = 80)
params = urllib.urlencode({
    'api_key': key,
    'forum': 'your-forum',
    'limit': '100'
})

client.request('GET', '/api/3.0/threads/list.json?' + params)

threads = json.loads(client.getresponse().read())['response']

for thread in threads:
    if thread['link'].startswith('https://your-domain'):
        post = thread['link'].replace('https://your-domain', '').replace('.html', '').replace('/', '.')

        client = httplib.HTTPSConnection('disqus.com', 443, timeout = 80)
        params = urllib.urlencode({
            'api_key': key,
            'thread': thread['id'],
            'order': 'asc',
            'limit': '100'
        })

        client.request('GET', '/api/3.0/threads/listPosts.json?' + params)

        comments = json.loads(client.getresponse().read())['response']

        cache = {}
        submitted = {}

        for comment in comments:
            date = datetime.strptime(comment['createdAt'], '%Y-%m-%dT%H:%M:%S').strftime('%b %d, %Y')
            cache[int(comment['id'])] = comment

            parent = comment['parent']

            if parent != None:
                while cache[int(parent)]['parent'] != None:
                    parent = cache[parent]['parent']

            options = {
                'post': post,
                'nick': comment['author']['name'].encode('utf-8'),
                'email': comment['author']['name'].encode('utf-8'),
                'content': comment['raw_message'].encode('utf-8'),
                'date': date
            }

            headers = {
                "Content-type": "application/x-www-form-urlencoded",
                "Accept": "text/plain"
            }

            if parent != None:
                options['reply'] = submitted[int(parent)]

            params = urllib.urlencode(options)

            client = httplib.HTTPSConnection('your-domain', 443, timeout = 80)
            client.request('POST', '/path/to/your/api/newComment', params, headers)

            submitted[int(comment['id'])] = int(client.getresponse().read())

一样用到了和前后端里面类似的处理评论嵌套的方法。

总结

这样我的简单的评论系统就完成了,老的评论也已经迁移过来,除了头像不太对。当然,还有一些需要优化的地方,比如说需要加入一个垃圾过滤API,这些我也许会在以后实现。

源代码

后端: node-comments

前端: Blog

在Android上运行Linux发行版

定制可以在 Android 手机/平板上运行的Linux发行版镜像,使用 chroot

在很久以前,我曾发现过一个App,叫 LinuxOnAndroid, 也在以前的米1上面玩过,那时候这个东西的确能够运行很多Linux发行版。可惜,后来这个项目停止维护了,里面的镜像都已经太老旧以至于更新一下都可能失败。后来换了米2,更新了 Android 5.0, 由于它要求 ARM PIELinuxOnAndroid 的镜像里面并没有开启这个,所以我认为它会失败而很久没有折腾。直到前天,在 ##Orz 大水群里面,有人告诉我在 chroot 环境里面是不会被这个限制影响的,因为这个限制是在Android的 linker 里面而不是内核里面,一旦切换根目录就会被替换掉。

正好,我也长期苦于在手机上没有好办法使用 git, 以及难以调试 nodejs Python 之类的东西,所以趁着中秋假期借鉴LinuxOnAndroid的经验自己跑起来一个Linux。

准备

由于我不可能把所有发行版都装一次,也不可能在各种手机上都测试一遍,所以我只能以如下的环境为例

  • armv7h 设备 (如MSM8974)
  • Android 5.x 和 busybox 完整支持 以及 root 权限
  • 至少 4GiB 的sdcard(内置存储)空闲空间
  • 至少 2GiB 的总运行内存
  • 自备终端模拟器和SSH客户端(如JuiceSSH)
  • 以安装和配置 ArchLinuxARM 为例

我们的目标是

  • 将目标Linux发行版安装至一个虚拟分区内
  • 能够在chroot下正常工作
  • 能够访问网络/sdcard
  • 能够通过ssh访问

注意,本文中的终端模拟器下执行的命令请全部在root权限下执行。(SSH下的命令请注意看说明)

如果你在过程中遇到了问题,您可以参考本文最后处的FAQ

Bootstrap

任何 Linux 发行版的安装,都要解决一个先有鸡还是先有蛋的问题,这个过程就是 bootstrap, 只不过在某些发行版的安装过程中被自动化了。在Android上安装,同样要经过这个过程。

首先,你得在你的sdcard(内置存储)上找一个空的目录以便管理文件,我们认为它的路径是 /sdcard/linux. 那么现在,打开终端模拟器,创建好这个目录。然后,运行下面的指令来创建一个4GB容量的空文件作为我们的虚拟磁盘

dd if=/dev/zero of=/sdcard/linux/root.img bs=1048576 count=4096

这条命令创建了一个包含4096块、每块大小是1M的文件,也就是一个4G的空文件 root.img, 接下来我们需要对它进行格式化以准备使用

mke2fs -t ext4 -F /sdcard/linux/root.img

这样我们就在这个空文件上创建了一个 ext4 文件系统,接下来我们把它挂载到 /sdcard/linux/root

mkdir /sdcard/linux/root
mount -t ext4 -o loop /sdcard/linux/root.img /sdcard/linux/root

然后我们需要下载 ArchLinuxARM 的bootstrap包。由于我们身处中国,从官网软件源下载速度并不是很快,所以我推荐大家使用USTC的镜像下载

http://mirrors.ustc.edu.cn/archlinuxarm/os/

打开这个URL,在里面找到你需要的bootstrap压缩包。比如说我的设备是 MSM8974 平台,那我就要下载 ArchLinuxARM-armv7-latest.tar.gz, 如果是其他平台则要把armv7换成其他的平台名称,比如说64位的手机应该下载 ArchLinuxARM-aarch64-generic-latest.tar.gz

我们假设这个文件下载后存储在 /sdcard/Downloads/ArchLinux.tar.gz

现在,把它拷贝进刚刚挂载出来的那个分区,并解压。

cp /sdcard/Downloads/ArchLinux.tar.gz /sdcard/linux/root/
cd /sdcard/linux/root
tar xzvf *.tar.gz

接下来我们要切换软件源到 USTC. 使用busybox里面的vim或vi编辑文件 /sdcard/linux/root/etc/pacman.d/mirrorlist, 将原来的 Server = xxxx 那一行注释掉 (前面加个#),然后在最前面重新添加一行

Server = https://mirrors.ustc.edu.cn/archlinuxarm/$arch/$repo

然后需要配置 DNS. 先用rm命令删除原有的 /sdcard/linux/root/etc/resolv.conf, 然后重新建立这个文件并编辑,键入DNS服务器配置

nameserver 8.8.8.8
nameserver 8.8.4.4

完成以后,我们就基本上完成了 bootstrap 过程,可以准备 chroot 进去了,但在这之前,我们得先把运行需要的目录挂载到虚拟根目录里面去

mount -o bind /dev /sdcard/linux/root/dev
mount -o bind /sys /sdcard/linux/root/sys
mount -o bind /proc /sdcard/linux/root/proc
mount -t tmpfs tmpfs /sdcard/linux/root/tmp
ln -s /proc/self/fd /dev/fd

最后一条指令是为了修复在子系统内执行一些包管理器会需要 /dev/fd 而出现的问题。如果不加这一条语句,那么在之后我们配置 pacaur 之类的从AUR安装软件的工具时会出现错误。

接下来,我们可以 chroot 进入刚刚创建好的基本系统了

chroot /sdcard/linux/root /bin/bash

配置基本系统

现在我们已经进入了这个基本系统,你可以看到命令提示符已经发生了变化。首先,我们最好先做一次全系统升级

pacman -Syu

然后安装一些工具

pacman -S base-devel vim sudo

接下来配置一个新用户,我们假设这个用户的名称是peter

useradd -m peter
passwd peter

运行第二行命令以后请键入为这个用户设置的密码。接下来用刚刚安装的 vim 编辑 /etc/sudoers, 添加这样一行

peter ALL=(ALL) ALL

这样 peter 就有了sudo权限。别忘了给 root 用户也设置一个密码,而且最好是强密码(反正平时用不到)。

然后我们准备启动 sshd 服务。先生成 ssh host key, 再启动sshd

ssh-keygen -A
/bin/sshd

这样你已经在后台启动了一个 ssh 服务。现在你可以关闭这个终端模拟器了,我们将使用 SSH 登入。

打开SSH客户端,新建一个链接,服务端IP写本地(127.0.0.1), 端口为默认的22, 使用刚刚新增的用户和其对应的密码登录(不要使用root)

配置网络

如果是在其他的Linux发行版上配置chroot环境,那么这个教程到这里就可以结束了。但是很可惜的是,我们使用的是 Android. Android 魔改的内核导致只有特定的用户组才能访问网络。所以我们在chroot的环境里面仍然需要配置对应的用户组才能在非root下正常使用网络。

sudo groupadd -g 3001 android_bt
sudo groupadd -g 3002 android_bt-net
sudo groupadd -g 3003 android_inet
sudo groupadd -g 3004 android_net-raw
sudo gpasswd -a peter android_bt
sudo gpasswd -a peter android_bt-net
sudo gpasswd -a peter android_inet
sudo gpasswd -a peter android_net-raw

然后退出ssh重新登录即可。

配置SD卡(内置存储)访问

由于我们创建的虚拟磁盘只有4G,可能不够存储使用,所以我们需要在子系统内访问SD卡。但是,Android同样是禁止普通用户直接访问SD卡的。所以,我们同样要进行相应的处理。

先打开一个终端模拟器,把SD卡挂载到虚拟根目录内

mkdir /sdcard/linux/root/mnt/sdcard
mount -o bind /sdcard /sdcard/linux/root/mnt/sdcard

然后关闭这个终端。

接下来一样要创建用户组,在ssh里面执行

sudo groupadd -g 1015 sdcard-rw
sudo groupadd -g 1028 sdcard-r
sudo gpasswd -a peter sdcard-rw
sudo gpasswd -a peter sdcard-r

然后尝试 ls /mnt/sdcard, 应该已经可以使用了。

配置区域和语言

在chroot出来的环境里面,我们没有 systemd 和任何其他初始化系统支持,所以不能自动设置语言。我们必须手动配置。

在ssh内编辑 /etc/locale.gen, 去掉 en_US.UTF-8 一行前面的注释符号,然后运行

locale-gen

接着编辑 ~/.bashrc (如果你使用了其他的shell那么请编辑这个shell的初始化脚本),添加这些到尾部

export LC=en_US.UTF-8
export LC_ALL=en_US.UTF-8

然后重新登录ssh即可。

配置启动脚本

这些挂载出来的虚拟分区在重启手机后会被取消挂载,如果每次都手动重新挂载和启动sshd会非常麻烦。所以,我们可以创建一个shell脚本,自动挂载分区并启动sshd

#!/system/bin/sh

mount -t ext4 -o loop /sdcard/linux/root.img /sdcard/linux/root

mount -o bind /sys /sdcard/linux/root/sys
mount -o bind /proc /sdcard/linux/root/proc
mount -o bind /dev /sdcard/linux/root/dev
mount -t tmpfs tmpfs /sdcard/linux/root/tmp

mount -o bind /sdcard /sdcard/linux/root/mnt/sdcard

ln -s /proc/self/fd /dev/fd

chroot /sdcard/linux/root /bin/sshd

这个脚本会将运行所需分区全部挂载上,并在 chroot 环境内启动一个sshd,这样你就可以直接使用ssh客户端连接到内部的系统了。这样,每次手机重启以后,你只需要开终端模拟器执行一下这个shell脚本就可以了。

以后

现在,一个Linux发行版已经在你的Android上运行起来了。你可以尽情地调戏,只要记住这是ARM平台而非和PC一样的平台。你甚至可以在里面运行一个VNC服务器,用 VNC 客户端连接,可以运行一个桌面环境,等等。

另外,我非常推荐修改ssh设置把ssh服务端的监听IP改成 127.0.0.1, 以防止别有用心之人。

如果需要从AUR上安装软件,你需要 pacaur 之类的命令,但是注意不要通过添加 pacman 软件源的方式安装,因为那些软件源都不支持ARM平台。要安装这类的工具,你得先从 AUR 上下载它们的 PKGBUILD, 然后放在当前目录里执行 makepkg -i 进行编译安装。

FAQs

Q: 如何使用初始化系统如systemd来管理服务?

A: 并不能使用,建议使用supervisord来替代。使用supervisord以后,可以在启动脚本里把执行sshd的代码改成执行supervisord的。

Q: SSH失败,提示PTY allocation failed on channel 0?

A: 尝试执行以下代码

umount /dev/pts
mount -t devpts devpts /dev/pts

后记

我在这个Linux环境里面运行了 node.jsPython 3.4,性能表现非常不错。但是,里面运行的 jekyll 较之PC却慢了很多,不过至少我终于可以用手机预览我的新博客文章了。在chroot里面,除了不能换内核以外,几乎可以把它当成一个完整的Linux发行版。加之 ArchLinuxARM 的支持非常好,你可以做任何PC上的 ArchLinux 能做到的事情,包括编译安装软件等等。

后来我在里面测试了 git, 几乎一切正常,但是在修改配置时,比如 git remote add, 如果当前目录在SD卡上,则会报错。此时,你应该在命令前加上一个 sudo

其他的,我也不想再一一赘述,总而言之,这就是一个全功能的Linux环境。

如此素质

几天前,我的一个老师说了这样一段让我细思恐极的话

你们这里的家长素质不行,有点事就投诉学校。像衡水中学那样的地方,从来都不会发生这样的事情。

这就比较有趣了。确实,有过类似好事者无理取闹的情况,但是不能排除学校确实存在问题的时候。可是 没有人投诉 这种情况,真的是所谓 素质高 的体现吗?

不能否认的是,如果从高考第一的角度来看,的确没有问题。学校这种东西,总是要追求各种率的:一本率,二本率……而这本身也是学生至少是家长所要的东西。所以某些高考机器学校的各种压榨时间的行为,就变得十分合理了,也没有什么好 叽叽歪歪 的。这大概就是这位同时身为校领导的老师所认为的。

可是不能忘了,不是所有学校都有这样的能力,也不是所有地方的家长都是这种心态。特别是 素质高 一些的家长,或者比较开放的地区,他们不会觉得高考是唯一的任务。而学校也是,你既要允许开设社团、学生会,又要学生和家长对学校言听计从,怎么可能?你既没有出100多个清北学生的能力,又想占用那么多时间,怎么可能?你给予了学生 非高考 带一些时间,他们自然就有时间去思考,自然就会对把现在的一切投入高考是否能改变是否有意义提出疑问;有了实际存在的学生会,他们自然就可能对你们提出异议,这难道是因为所谓素质低吗?

在我看来,对身边的事情存在疑问并能看出一定的不合理之处才是真正的 有素质 的体现,而对其他人一直言听计从那是封建思想塑造出来的愚民才会有的行为。这又让我想起前两天在知乎上看见的一个问题。

这个问题是这样的,是一位题主因为对一些政策提出疑议而被劝 要多角度看问题。问题本身没什么,可是有些答主的回答实在是可怕。他们说,因为你不能改变什么,所以有疑议只会让自己不开心,所以你要学会从某些人的角度考虑,要让自己相信一切都是为了你好,要让自己没有疑议,这才能活得开心。

呜呼!这种回答让我觉得仿佛回到了多少年前。一个老大哥一声令下,举国上下无不惊骇震慑,立即不折不扣地执行,还满口叫好觉得这一切都是为了自己。唔,似乎并不是很久以前哇。

的确,这样可以让自己什么都不想。不想太多,自然就开心了。可是人如果不想的话,那和咸鱼还有什么区别?和傻子一样过每一天,开心是开心了,但这种感觉就像是被圈养一样。反正,我是不会对这种生活感到任何程度上的开心的。

然而我们从小就被教育要这样。老师学生家长都习惯于填鸭式的学习和对权威的服从。即使是在不被别人管到的时候,那也是 抄作业,向答案基本正确的学霸抄作业,这是从既定的结果推导原因和过程,而不是从原因主动地推导结果。这常年累月,甚至可能一代代人地积累,就会导致这样的思维定势,就是凡是xxx做的都是有道理的,然后就仅止于此了。即使政治课强调再多次要独立思考要一分为二,也没什么卵用。也许有可能 有疑议 的人,也只能是那些剩下的学霸们了吧。而他们后来会成为什么呢?我想我不说大家也明白了。

而现在的小学老师过早地向学生灌输等级意识、权力意识、官僚意识,学生却只能无条件地接受,这就使这个问题的影响更加严重。所谓的 少先队,就一直在作着这样的恶。一周以前我妹妹因为别人不尊重这个 当过班长的人 而气了一整天,并且一点都看不起现在的班长和成绩差一些的同学。为了给考个所谓的大队长,简直勾心斗角。因为在考试材料上得知 少先队由共青团领导,竟然又和我赌了一天的气。而对于这些存在本身的合理性,他们已经没有去质疑的可能了,因为这已经是他们思想里面固化的东西了。 有组织,有纪律,你和小学生去说,呵呵……这真的不是素质!服从,除了在军队里面,绝非现代人所要有的素质。

至于这样的被固化了不独立思考的习惯的人,知乎上一大堆一大堆。不要觉得知乎是什么好地方,那上面有几个人真的独立思考?某Wall的存在也是因为国情,奇葩广电也是因为国情,反正什么都是国情。也有另一个极端,就是x国就是灯塔,凡是x国的政策都是先进的,其他都是渣。从来不对自己相信的东西提出任何问题,这就是可怕的地方。

有的人说中国人缺乏信仰,我不敢苟同。我不能理解一个人有一个坚定的信仰会是什么好事情。(当然,为了不犯 错误,我曾在考试作文中不止一次宣扬要有信仰。)万一你信仰了不合理的东西呢?万一你信仰了一种阴谋呢?如果一个人对自己的信仰毫不怀疑或者不被允许怀疑的话,那就意味着这个信仰的掌控者控制了全部信仰者的全部思想和行为。这种情况下,所谓的信仰者,无非就是行尸走肉而已。可怕的战争无一不是由这样而引发。所以这样的人,你会觉得他们 有素质 吗?

没有人可以知道自己现在所相信的或者身边的人和事是不是合理的,所以我们才需要怀疑。人的知识不是一成不变的,所以我们才需要怀疑。怀疑是整个现代人类社会的根基之一。离开怀疑,什么民主、监督,连实现的可能都没有,直接成了一纸空文。你觉得没人投诉代表素质高,其实你最后培育出来的学生都是不知道自己要做什么的高分的僵尸。为了不让自己多想,就用各种各样的活动来填满自己,别人看起来很学霸,其实自己都不知道自己想做什么,只会跟着别人的脚印走,因为老师说过的,要听话。而真正有能力的学生,在高分的同时,往往也是让学校觉得头疼的那种。

生当作人杰,死亦写作业。长江后浪推前浪,前浪还在赶作业。

真的应该停一下,好好想想,质疑不合理的东西,包括自身的和环境的,放掉所谓的坚定信仰,做一个真正的自己,这才是一个人真正的素质啊。至少当百年之后,你可以问心无愧地像音无结弦一样大声说

我们所活过的人生并不是没有意义的,那是真正存在的,只属于我们的人生,是真真实实存在过的啊!

话又说回来,作为校领导,本来就有解答疑问的责任,你们作出的决定自然要你们来解释,我也想不明白这有什么问题。

我就不说什么救救孩子了。反正,俺是没啥素质的。