zshrc 启动速度分析和优化

由于常年不科学的使用和随便塞东西,我的 .zshrc 里有太多太多的各类语言、SDK 的启动逻辑,因而它逐渐变得不堪重负起来。今天终于受不了了,我决定对它进行整理,移除部分太慢的代码,并且将部分不需要实时加载的东西懒加载。

速度优化的前期准备

要开始优化,首先需要有科学的评估速度的方法,这将使我们能够找到速度的瓶颈。

测算总启动速度

使用 time 命令,可以测算 zsh 的启动时间:

$ \time zsh -i -c exit
        6.62 real         3.23 user         2.29 sys

可以看到,我的 zsh 启动时间需要 6.62 秒,可以说是十分糟糕的速度了。作为对比,不加载任何启动命令脚本的 zsh 启动速度飞快,甚至只需要 0.01 秒:

$ \time zsh --no-rcs -i -c exit
        0.01 real         0.00 user         0.00 sys

设定优化目标

一个合理而可以达到的目标对速度优化也至关重要。今天,我的目标是不修改第三方代码的情况下,将加载时间优化到一秒以内,而尽量不损失任何功能。我们评估时间所用的方法就如同上文所述,以 time 测算的 real 时间为准。

找到速度瓶颈

找到速度瓶颈的方法通常是运行“性能评估”,也就是 Profile。由于 zshrc 是一个 bash 脚本,我需要寻找一些 zsh 脚本的 profile 方法。

一番查找之后,找到了 Profiling zsh shell scripts 这篇文章。按照文章中描述的方法,我们在 .zshrc 的最前面加入:

PS4=$'\\\011%D{%s%6.}\011%x\011%I\011%N\011%e\011'
exec 3>&2 2>/tmp/zshstart.$$.log
setopt xtrace prompt_subst

在最后面加入:

unsetopt xtrace
exec 2>&3 3>&-

然后执行 zsh -i -c exit 让 zsh 运行一遍初始化。执行完成后,你应该可以在 /tmp 下看到输出的结果:

$ ls -l /tmp/zshstart*.log
-rw-r--r-- 1 oott123 wheel 470745 Jun  9 16:26 /tmp/zshstart.8854.log

这个日志文件已经可以看了,只不过,人类似乎很难阅读……万幸的是,文章的作者也给出了一个工具,用于把输出的文件转换为 KCachegrind 可读的 callgrind 文件。然而,它是 OCaml 写的,也没有提供预编译二进制文件,我们必须先安装 OCaml 来编译运行它。为了速度,那就装吧!

由于我们在 macOS 下工作,通过 brew 可以很轻松的安装 OCaml、opam和 QCacheGrind(这里使用 QCacheGrind,效果和 KCachegrind 应该一样):

brew install ocaml
brew install opam
brew install qcachegrind --with-graphviz

然后使用作者提供的工具 zshprof 将日志文件生成 callgrind 文件,并使用 QCacheGrind 打开它:

git clone https://github.com/raboof/zshprof.git
cd zshprof
opam init # 完了,我们还没开始优化,又有一个程序往 .zshrc 里写东西了……这就是为什么它越来越卡
eval `opam config env`
opam install ocamlfind

# ocamlfind ocamlopt -linkpkg -thread -package str ZshXtraceToCallgrind.ml
ocamlfind ocamlopt -linkpkg Callgrind.ml -linkpkg ZshXtrace.ml -thread -package str ZshXtraceToCallgrind.ml
./a.out < /tmp/zshstart.8854.log > zsh.callgrind
qcachegrind zsh.callgrind

记住这个艰辛的过程,我们在优化途中会经常运行它,以检查优化效果。

打开 QCacheGrind,找到你的 zshrc 文件的源码,点击按 micros 排序:

QCacheGrind 主界面,其中显示了每行源码的耗时。

(由于我个人将自己的所有 rc 代码都写到了一个叫 .shellrc 的文件里,因此这里的截图是 .shellrc 这个文件的。)

很显然,大量类似 dvmphp-version 这样的版本管理工具初始化命令和他们的补全函数消耗了绝大多数时间。我们就从这些函数入手,优化我们的 shell 加载速度。

移除不必要的进程创建

不看不知道,一看吓一跳。就拿下面两句来看:

[[ -s "$(brew --prefix dvm)/dvm.sh" ]] && source "$(brew --prefix dvm)/dvm.sh"
[[ -s "$(brew --prefix dvm)/bash_completion" ]] && source "$(brew --prefix dvm)/bash_completion"

光这两句,就执行了 4 次 brew --prefix dvm,而这个命令在我的机器上执行一次需要 0.7 秒:

$ \time brew --prefix dvm > /dev/null
        0.71 real         0.37 user         0.19 sys

为了加载 dvm 而执行的 brew 就用掉了 4*0.71=2.84 秒,这还不算 dvm 本身的加载时间!而对于我而言,brew --prefix dvm 的结果执行一万次也不会改变,一定是 /usr/local/opt/dvm ——因为我知道我不会移动默认的 prefix 位置。那么,将 brew --prefix 命令都替换为字符串:

[[ -s "/usr/local/opt/dvm/dvm.sh" ]] && source "/usr/local/opt/dvm/dvm.sh"
[[ -s "/usr/local/opt/dvm/bash_completion" ]] && source "/usr/local/opt/dvm/bash_completion"

再跑个分试试:

$ \time zsh -i -c exit
        1.62 real         0.93 user         0.69 sys

哇,真是成效显著,一下就从 6.62 秒缩短到了 1.62 秒,整整减少了 5 秒钟!

移除 brew 之后 QCacheGrind 的样子

重新跑一次 profile,也可以看到,那些明显拖慢启动速度的项目已经没有了。顺便吐槽下:Ruby 和 brew 启动也太慢了吧……

现在,我们还剩下一些别的命令初始化和命令补全的耗时。这些命令看起来很好,只是他们的内部耗时相对较大。

懒加载命令和补全功能

为了不影响功能,又不修改第三方代码内部实现,我们无法很好的优化那些非常缓慢的命令初始化过程。但是,大多数情况下,我不会用到其中大多数命令。因此,我希望实现“懒加载”功能,在我首次使用这些功能的时候,zsh 帮我初始化他们。

懒加载的好处是启动快,缺点是运行的时候会比较慢。但这个慢是分散的,不会全部都卡在启动的那几秒钟里,所以我认为效果损失是可以接受的。那么,怎样懒加载他们呢?

一个很简单的方法是,把命令换成一个占位函数,然后在这个函数中再去执行真正的二进制文件。比如:

dvm() {
    # 移除占位
    unfunction "dvm"
    # 加载真正的 dvm
    source "$(brew --prefix dvm)/dvm.sh"
    # 加载 dvm 的补全
    source "$(brew --prefix dvm)/bash_completion"
    # 执行真正的 dvm 命令
    dvm "$@"
}

(上述代码实现参考自:Speed up initial zsh startup with lazy-loading,基于 CC-BY-SA 使用)

但这样,第一次使用 dvm 的时候,就没有命令补全了!于是,命令补全也用同样的方法做懒加载:

__lazycomp_dvm() {
    # 移除占位
    compdef -d dvm
    unfunction dvm
    source "$(brew --prefix dvm)/bash_completion"
}
compdef __lazycomp_dvm dvm

这样,在第一次按 Tab 的时候,就会加载 dvm 的补全。由于不同命令的补全逻辑不一样,所以还是没法第一次执行补全命令,但多按一次 Tab 键至少比多执行一次命令好多了。

把这两个懒加载方法写成函数:

my_lazyload_add_command() {
    local command_name=$1
    eval "${command_name}() { \
        unfunction ${command_name}; \
        _my_lazyload_command_${command_name}; \
        return ${command_name} \"\$@\"; \
    }"
}
my_lazyload_add_comp() {
    local command_name=$1
    local comp_name="_my_lazyload__compfunc_${command_name}"
    eval "${comp_name}() { \
        compdef -d ${comp_name}; \
        unfunction ${comp_name}; \
        _my_lazyload_comp_${command_name}; \
    }"
    compdef $comp_name $command_name
}

再把 dvm 等不太常用的命令初始化逻辑用这两个函数写出来,比如说这样:

_my_lazyload_command_dvm() {
    source "/usr/local/opt/dvm/dvm.sh"
}
_my_lazyload_comp_dvm() {
    source "/usr/local/opt/dvm/bash_completion"
}
my_lazyload_add_command dvm
my_lazyload_add_comp dvm

搞完之后再跑个分试试:

$ \time zsh -i -c exit
        0.37 real         0.24 user         0.18 sys

哇,已经只有 0.37 秒了。再看看 Profile,大头已经到了 oh-my-zsh 里:

最后的 Profile 结果

打开 zsh 一看,确实感觉快多了。试试那些被我们 lazy load 的命令,用起来感觉也没有什么延迟。优化初见成效,收工。

对了,完成之后记得把一开始我们添加到 .zshrc 中的几行 Profile 代码干掉。否则,它会创建一堆临时文件,并且它本身也会影响一点点速度。

如何让你的 .zshrc 更高效

这次优化,也让我看到平时写 shell rc 文件的一些常见误区,这些问题可能导致 shell 启动慢到爆表。总结说来,主要是这几点:

  • 尽量避免在初始化脚本中调用外部进程,特别是脚本语言的解释器,比如 node, ruby。他们的冷启动时间非常长,长到你怀疑人生。
  • 避免重复执行语句,特别是调用外部进程的语句。
  • 避免增加不必要的初始化代码。比如调用 nvm 初始化 node.js 版本。应当利用懒加载或者 avn 等方式来做这个事情,或者干脆写死环境变量。
  • 尽量多使用懒加载,避免加载不必要的函数。

其实今天本来还想顺便把 .zshrc 给模块化,并加入版本控制的,但调优它本身花掉了太多时间,这篇文章也足够长了,就下次再弄吧。

知乎“分享”功能可能正危及你的隐私

如果你和我一样是知乎和 QQ 的深度用户,你可能对这样的消息格式很熟悉:

一条知乎的分享消息

这是从知乎安卓客户端(因为我是安卓用户,所以本文只讨论安卓客户端的问题,iOS 不清楚,可能一样可能不一样)分享到 QQ 的文案格式。让我们仔细看看这个链接:

https://www.zhihu.com/question/61427877/answer/361977341?utm_source=qq&utm_medium=social&utm_member=YzA1N2VkNTNiYTMyMmMwZDdiODYxYmI0NDRiOWZlYTY%3D%0A

一个分享链接

紫红色的部分就是打开这个页面所需要的链接。里面有两个数字,分别代表问题、回答,这样,这个链接就会打开一个回答的页面。这很好,完全没有什么问题。

接下来,绿色的部分:utm_source=qq。到这里,事情变得很微妙。这表示这个链接是通过 QQ 分享出来的。蓝色的部分:utm_medium=social,这表示这个链接是通过社交平台分享出来的。你可以试试把这两个参数删掉,这并不影响你打开这个回答页面。

最重要的问题在黄色的部分:utm_member=Yz.....%3D%0A。你会发现这个部分很长,而且看上去似乎没有什么意义。真的如此吗?我将简单的演示如何通过这个字符串来获取你的知乎个人页地址。

做一些转换

把这串乱码做 URL 解码,再做 base64 解码。此时,它变成了一串 16 进制字符串——好的,还是乱码。但这串乱码已经有用了,打开 https://zhihu.com/people/c057ed53ba322c0d7b861bb444b9fea6 ,你会看到,它跳转到了分享人的个人页。

感谢兮嘉的出演

这样就完成了一个“从分享链接找到分享人的知乎帐号”的过程。这其实很严重——如果你把链接分享到一个群里,那么当群友打开这个链接时,知乎就知道了你和他们是群友;群友也知道了你的知乎帐号。这是十分明显的隐私信息泄漏。

为了讨论隐私泄漏的问题,我们先来聊一聊追踪是如何实现的。如果你已经了解了 utm 追踪,你可以跳到第二节“这有什么问题?”继续阅读。

追踪如何实现?

链接

万维网用户对“链接”都非常熟悉。简而言之,万维网通过链接联系在一起,通过链接来在不同网站、不同应用之间导航。

一个典型的链接,如同一开始我们提到的那个,是由协议、主机、路径、参数和一些其它部分构成。其中,协议和主机用来确定请求该如何、发送到哪个服务器,而路径和参数则是直接发往服务器,由服务器用于决定展示什么内容。

在分享时,知乎,或者其它别的什么 App,大致都会将当前页面的内容生成一个链接,然后再将链接分享到其它 App 中。

utm 参数

上文我们提到了,参数是直接发往服务器的,而分享的时候可以自定义参数,意味着我们可以通过在链接中加入一些特殊的参数。这些特殊的参数静静的躺在链接里,等待别人点击——而一旦被别人(比如你分享的好友)点击,这些参数就将发往服务器,服务器就知道:哦,这个用户的请求有这些参数,意味着他是从分享链接点击过来的。

而 utm (Urchin Tracking Module)参数是业界通用的、用于处理追踪的参数。通常而言,这些参数并不影响你打开的页面是什么样的(对用户无感知),只是用来统计和追踪。这些参数通常以 utm_ 开头。(扩展阅读:https://en.wikipedia.org/wiki/UTM_parameters

就像知乎做的那样,分享的时候增加了 utm_source=qq 这个参数,其它用户点击的时候,这个参数会被发送到知乎服务器上。这样,知乎的服务器就知道了,你的好友是通过 qq 分享的链接访问的知乎。

追踪的积极意义

如果你理解了前面我所说的内容,你很容易想到为什么大家会热衷于使用追踪技术。产品经理想了解从哪个渠道的分享到达率最高、哪个渠道的转化最厉害,或者可能根据来源渠道给予各种奇奇怪怪的针对性优化(或者负优化……)。甚至说,有一定规模并建立起一定的监控机制之后,如果某个渠道出现了问题,也可以迅速发现。

这有什么问题?

如果说追踪对用户没有感知,并且又有那么多好处的话,为什么我要说知乎正在危及你的隐私呢?事实上,由于链接参数的可见性(对第三方平台、浏览器等工具是可见的),这些参数可能被泄漏给第三方。此外,对第一方(应用开发者,比如知乎)而言,许多所有用户操作都属于用户的个人隐私,需要对用户显式说明。

我将在这里简单的说明几个较为明显、容易想到的问题。这些问题并不一定实际存在——我也不知道他们是不是存在,我只在此提出:这些技术是完全可行的,并且知乎也可以简单的避免这些问题。

跨平台的用户追踪

对于每个用户而言,不管你分享多少次,只要你不退出或者更换帐号,utm_member 是不会变化的。这可以被唯一标识用户,我们称之为“知乎用户标识”——当我们看到这个用户标识,就知道它是你。虽然这个链接是 https 的(这样运营商和中间服务器不会看到链接),但除了你和知乎,还有别人可以获取这个链接。

比如你分享到 QQ 上,那么 QQ 就可以知道你的知乎用户标识;你把链接复制到微博,那么微博也知道了你的知乎用户标识;你用浏览器打开,浏览器也知道了你的知乎用户标识。第三方平台完全可以把你在他们平台上的帐号和你的知乎用户标识关联起来,实现跨平台追踪。

可被任何人识别的用户标识符

文章的开篇部分我简要展示了如何从分享链接中获取你在知乎的用户信息。这事实上没有任何技术门槛,任何人都能进行尝试(如果你对技术一无所知,你可以搜索“在线 base64 解码”、“在线 URL 解码”,能搜索到很多在线工具代劳)。假设你在知乎上是超级大V,但身边的人都不知道、你也不想让他们知道。但现在,只要你通过安卓知乎客户端从 QQ 上分享任何内容给你的朋友,大家都知道你就是知乎大V了……

应该怎么办?

相信你已经理解了问题是如何存在的(如果没有理解那就算了……)。那么如何避免这种隐私问题呢?这里我也从用户和运营方(知乎)的两个方面来思考应当如何处理。

用户怎么办

对用户而言,移动端事实上缺乏这类隐私防范的工具。就当前这个事件来看,我发现通过“复制链接”的方法可以获得相对干净的链接,从而摆脱 utm 的追踪。

在桌面浏览器上,我推荐有些隐私洁癖的朋友使用 Neat URL 这个扩展。它可以移除很多无用的 URL 参数,并且有丰富的自定义功能。(利益相关:我是一个 Firefox 用户,我也推荐每个人使用 Firefox,所以不知道 Chrome 下应该怎么办~)

知乎怎么办

知乎应当从整体考虑放弃追踪特定用户的分享行为。如果你非要那么做,至少请:

  • utm_member 字段每次分享都不一样(避免追踪特定用户)
  • utm_member 字段无法被第三方轻易反解出用户唯一标识

举个我认为比较好的做法:在客户端随机生成一个唯一字符串,将该字符串发送到服务器上,让用户与其关联。每隔一段时间(比如 4 小时)重新生成该字符串。

总结和反思

事实上在发现这个问题之后,我有和一些朋友描述我的发现,并且也试图向知乎反馈该事件,但并未得到什么响应。我想,大概从知乎到用户,所有人都不太理解这为什么对隐私有所伤害。

当然,知乎和国内大部分 App 一样,在隐私方面的问题远不止我提及的这一点,只是这一点非常容易验证而又十分明显,才这样公开的提出来。知乎在国内的移动市场上算是比较有情怀的(起码以前这样……),也希望知乎能够有足够的内部力量推动关注这些事情。

国内在隐私方面的关注实在是太少,而又很难有合适的渠道来了解、学习这些和自己生活息息相关的事情。这篇文章或许有不少片面或者理解不到位的地方,也请各位指正。

Safari 下同页面创建的视频超过 16 个则无法播放

最近在做一些视频相关的东西。在 Safari 下发现这样一个问题:<video> 标签创建多了,会导致同一个页面内后续的视频都无法播放。不管是 iOS 的 Safari,还是 macOS 下的,都存在同样的问题。尤其是 iOS 下,超过 16 个的视频几乎必定无法播放;效果就是显示一个播放按钮并且有一条斜线。

无法播放的视频截图

通过 video.error来检查最后的错误,得到的是 MEDIA_ERR_DECODE;同时,video.play() 方法会 reject 一个 AbortError。在 Google 检索了很久也没有得到有用的信息。

为了演示这个 bug,一个简单的示例的源码被我放在了 gist 上。你可以通过在 Safari 上打开 RawGit 提供的链接 来测试这个 bug。你只需要点击“测试”,并在视频开始播放后再次点击“测试”,如此循环 16 次,便能得到和我上一张图片一样的结果。

示例的代码如此简单,以至于我认为这个 bug 是几乎不可能发生的;它仅仅是调整了一个 DOM 的 innerHTML 来创建一个 video 元素(事实上无论通过何种方法创建,都会有这个问题;playsinline 属性似乎是必要的),然后再删除它,循环 16 次而已。

hello.innerHTML = '';
var src = 'https://cdn.rawgit.com/mediaelement/mediaelement-files/4d21a042/big_buck_bunny.mp4';
hello.innerHTML = '<video id="video" webkit-playsinline playsinline controls src="' + src + '">';
setTimeout(function () {
    document.getElementById('video').play();
}, 500);

目前,我没有发现任何能够解决这个 bug 的方法。可能唯一的办法就是,播放 15 个视频之后,刷新一下整个页面吧。

为熊猫 TV 直播生成带弹幕的录播视频

最近在沉迷谜之声直播。由于有时候下班比较晚,赶不上开播,又偶尔对直播游戏感兴趣的,就会跑去找录播来看。这个主播十分好心,会在贴吧里上传自己的录播视频。然而由于主播自己的录播视频采用 obs 串流的同时录制,对弹幕肯定是无能为力的,于是就想着能不能通过什么办法把弹幕加上去。所幸,办法还是有的。

现在熊猫直播全站 https 后,弹幕数据都是走的 websocket 了,通过 Fiddler 之类的抓包工具可以轻松抓到。不过,知乎上有一篇详细的回答说明老版本弹幕格式,也有一些现成的项目用来解析弹幕,所以这里就不关注弹幕数据格式了。总而言之,我把这些东西封装成了一个 npm 库 pandan,可以通过 node.js 简单的获取一个房间的实时弹幕了。比如:

let room = new (require('pandan').Room)(426346)
room.on('room-danmaku', function ({data}) {
  console.log(`${data.from.nickName}: ${data.content}`)
})
room.join()

有了弹幕数据自然就好办多了。从抓到的数据包来看,有主播开播、下播事件,送礼、送竹子等事件,完全足够我生成弹幕了。通过一段小脚本,可以把弹幕格式转换成下面这个样子的 xml:

...
<d p="1830.6300048828,1,25,16777215,1415241251,0,Da8b0a0d,668464499">再见了我的青春</d>
<d p="1477.6800537109,1,25,52479,1415241254,0,35b92ad0,668464527">虐啊</d>
...

脚本比较丑,就不贴了,相关参数的解析可以看这篇文章。这里有个小问题,由于直播通常有几秒钟的 buffer,所以弹幕整个时效性会稍微延后一些。这个问题在你观看直播的时候不容易发现(因为大家都有延迟),但生成录播之后弹幕会很明显的延后。当然,解决起来也十分简单,将弹幕时间轴往前平移几秒钟就可以了。

然后祭出大杀器:danmaku2ass,一个能将弹幕转换成 ass 字幕的工具。执行类似这样的命令:

python3 danmaku2ass/danmaku2ass.py -o 2017-07-17-1942-09.ass -s 1152x648 -fn "Microsoft YaHei" -fs 30 -a 0.8 -dm 7 2017-07-17-1942-09.xml

就能生成 ass 字幕了。使用任何带字幕功能的播放器,都可以播放啦~看个例子:

example image

当然,并不是所有主播都会自己上传录播视频。如果你刚好也有这种需求,可以尝试在连接弹幕服务器之后,获取开播事件,并在开播之后通过 you-get 来获取直播录像。

User Mode Linux – 一个用户态的 Linux 内核

User Mode Linux 可以在用户态启动一个 Linux。这使我们能在类似 OpenVZ 虚拟化技术的系统上,使用最新的 Linux 内核;甚至可以在非 root 用户下启动。

但有些 OpenVZ VPS 的 TOS 可能不允许你这么做;另外,这样可能会有一定的性能损失。

准备 rootfs 镜像

我们找一台 Ubuntu Server 来准备 rootfs。这个时候需要有 root 权限,因为你需要 mount 一个 loop 文件系统,chroot 改点东西之类的。

# 下载镜像
wget http://uk.images.linuxcontainers.org/images/alpine/3.1/amd64/default/20170305_17:50/rootfs.tar.xz
# 安装解压软件
apt install xz-utils
# 创建镜像,32 M 对于 alpine 是够用的
dd if=/dev/zero of=rootfs.img bs=1M count=32
mkfs.ext4 rootfs.img
# 把镜像挂到 loop 上
mkdir rootfs
sudo mount -o loop rootfs.img rootfs
sudo tar xvf rootfs.tar.xz -C rootfs
# 给 root 设个密码
sudo chroot rootfs /bin/sh
  passwd
  exit
# 清理
sudo umount rootfs
rmdir rootfs

此时 rootfs.img 已经做好了。

编译内核

我们继续用 ubuntu server 来编译 UML 内核。

sudo apt install build-essential kernel-package fakeroot libncurses5-dev libssl-dev ccache
# 抓内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.10.1.tar.xz
tar xvf linux-4.10.1.tar.xz
cd linux-4.10.1
make defconfig ARCH=um
make menuconfig ARCH=um
# 这里会出来一个界面让你配置内核,有茫茫多的参数可以选
# 大概找一些 linux 内核编译的文章可以参考吧
# 主要是 User Mode Linux 有一些特殊选项可以注意下,比如强制静态编译什么的
# 然后就开始正式编译啦
make ARCH=um
strip linux # 移除调试符号

编译过程可能比较慢。有时候会有一些库找不到啥的,apt 装一下就好。

获取 slirp

slirp 是用来在没有 root 的情况下联网的。

apt source slirp

这里获取到的 slirp 放到目标机器上编译,不需要在 ubuntu server 上搞了。

cd slirp-1.0.17/src
./configure
make CFLAGS="-I. -DUSE_PPP -DUSE_MS_DNS -DFULL_BOLT -fno-strict-aliasing -Wno-unused -std=gnu89" clean all

启动 UML

现在万事俱备,只欠东风了。

# 在 UML Host 上
# 把一些二进制文件放好
slirp=/home/oott123/uml/slirp/slirp-1.0.17/bin/slirp
uml=/home/oott123/uml/kernel/linux-4.10.1/linux
rootfs=/home/oott123/uml/alpine/rootfs.img
cow=/home/oott123/uml/machine/demo/data.cow # 放 copy-on-write 数据的地方
export TMPDIR=/tmp
$uml rw ubda=$cow,$rootfs mem=128M eth0=slirp,,$slirp

配置网络

启动之后你可能会发现没有网络。不用担心,改改网络配置:

# 在 UML 里
vi /etc/network/interfaces
#####
auto eth0
iface eth0 inet static
address 10.0.2.15
gateway 10.0.2.2
netmask 255.255.255.0
dns-nameservers 10.0.2.3
hostname $(hostname)
#####
/etc/init.d/networking restart
echo 'nameserver 114.114.114.114' > /etc/resolv.conf
# 测试网络
wget -O- myip.ipip.net
# 安装软件
apk update
apk add curl
curl myip.ipip.net
# 关闭 UML
halt

与主机交互

或许你需要将 UML 内的端口转发到主机上。

# 在主机上
vi ~/.slirprc
redir 2222 22

或许你需要访问主机的文件系统。

# 在 UML 上,把 UML 内的 /mnt/home 映射到 /home
# 注意权限问题、注意两边不要同时读写
mkdir /mnt/home
mount none /mnt/home -t hostfs -o /home

2016: 我的浏览器 A – Z

A: https://aoishi.[REMOVED] 自己服务器的管理面板
B: http://bbs.i-cassell-you.com/forum.php 还是卡院
C: http://civitas.soobb.com/ 多少年没上了……
D: https://www.dnspod.cn/ DNSPod 有几个域名在上面
E: http://easy-admin.[REMOVED] ???已经是我记不起来的东西了
F: http://fiddle.jshell.net/ 呃……其实我用得不是很多
G: https://gitlab.com/ Ainou 的代码托管在那,所以经常去
H: http://hicivitas.com/ 以前兼职公司的官网
I: http://www.ip138.com/ …………大概是经常看自己 ip
J: http://jd.com/ 已经不怎么上淘宝了,狗东比较舒服
K: http://kuler.adobe.com/ 一个配色工具,可能是因为在收藏夹吧,平时不怎么用
L: http://logcg.com/ 这是谁的博客啊……完全不记得访问过了
M: https://mp.weixin.qq.com/ 微信公众平台……嗯最近可能是上得比较多
N: http://notes.jetienne.com/ 这又是谁的博客了……不记得了
O: http://openshift.redhat.com/ 我感觉今年我应该没上过
P: http://piao-tech.blogspot.com/ 怎么越来越离谱了
Q: http://qzone.qq.com/ ……啥。。
R: http://redis.io/ 嗯……倒是经常去查文档
S: https://suiyu.xyz/ 偶尔会写日记
T: http://tower.im/ 好用的团队协作工具呢……不过现在也不怎么用了
U: http://upyun.com/ 偶尔会上去配一下 CDN
V: https://v2ex.com/ 毫无悬念
W: http://weibo.com/ 其实我最近比较少上了……
X: http://xiumima.com/ 呃……社工库么
Y: http://yeah.net/ 偶尔会上网易邮箱看看
Z: https://zhihu.com/ 知乎倒是常去看的

总之,通过浏览器,可以看出一个趋势:我使用浏览器浏览不同的 web 网站的时间越来越少了——大部分网站我根本想不起来。而更多的时间,可能花在了刷手机上。移动互联网近年来确实是势不可挡,我也不得不沦陷了。

唯一不变的,大概是 Web 的信仰——以及,今年我还在坚持使用 Firefox 浏览大部分网页。

清空 Mac QQ 的最近表情列表

更新 Mac QQ 之后,最近发送过的表情变成了一大片惊讶,如图:

惊讶的 Mac QQ

不管发什么表情这些顺序都不会变,简直坑爹。去 QQ 的存储目录找了一下,发现删掉以下文件后就会好:

HistoryFacesStack_V52.archive
HistoryFacesList_V52.archive

这些文件位于 ~/Library/Application Support/QQ/(QQ号)/CustomFaceConfig.db(非沙盒)或 ~/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/(QQ号)/CustomFaceConfig.db(沙盒)下。

删了以后,重启两次 QQ,果断好了:

不再惊讶的 Mac QQ

Unmanaged Exports 在中文系统下报错 syntax error at token ‘{‘ 问题

UnmanagedExports 是一个可以在 C# 下生成类 c api 的 dll 的工具。

不过这玩意儿在中文系统下编译,可能会产生如下报错:

C:\Users\Administrator\AppData\Local\Temp\tmp6C70\adddll.il(60) : error : syntax error at token '{' in:   { AddDll

解决方法很简单,换成英文系统即可。

至于啥原理,我也不知道。写下来避免后人踩坑……如果有哪位同仁知道怎么回事,也请告诉我~

记 VestaCP 的 Let’s Encrypt 自助签发配置

主要原料是 https://github.com/interbrite/letsencrypt-vesta 这个脚本。

然后写一个 python 脚本用来给用户自助签发

#!/bin/env python
import sys
import re
import os
import subprocess

args = ["/usr/local/bin/letsencrypt-vesta", "-u", os.environ['SUDO_USER']]
for i in sys.argv[1:]:
    if re.match('^[a-z0-9][a-z0-9.-]+$', i):
        args.append(i)
subprocess.call(args)

给用户 sudo 权限

%users ALL=NOPASSWD:/usr/local/bin/sign.py

然后让用户执行 sudo /usr/local/bin/sign.py 域名 即可。

重新分享我的 RSS 文章订阅

由于最近犯懒严重,所以这玩意儿也很久没更新了……

之前我曾在博客分享过我的 Star 文章订阅,坚持了一段时间之后,就开始用 kindle 读 RSS 了,所以就没法去 Tiny Tiny RSS 给喜欢的文章加 Star。不过最近我发现,用 kindle 读 RSS 的效率实在是比不上用手机 App 读,于是又切换回了 Tiny Tiny RSS。

这次把 Tiny Tiny RSS 重新配置了一遍,然后改用“Publish”功能取代之前用的“Star”功能,并且加了一些 Article Nots,这样订阅就会变得更加像“分享”一些。

RSS 地址:http://share.best33.com/feed/people/oott123.rss