Live USB for Parted Magic

上次笔记本出了点小意外无法正常启动,当时手忙脚乱找不到现成能用的系统修复工具(只有一个安装了 WinPE 的 U 盘,无法引导 Linux 系统),事后有点忧虑,于是买了个 8G 的 U 盘,参考 Gentoo 官方的 Live USB 制作教程,在 U 盘上装了个 Parted Magic。这货工具很齐全,分区、数据恢复、磁盘镜像这些常用的工具都有,而且还能连接网络,用 Firefox 查资料什么的……

制作过程比我想象中简单(尝试之前请备份好 U 盘的数据):

  1. 用 fdisk 给 U 盘分区(分了 1G 给 Parted Magic,用 FAT32 格式,剩下的空间格式化成 NTFS 分区,用于日常文件搬运)

  2. 执行 sudo dd if=/usr/share/syslinux/mbr.bin of=/dev/sdb 把 syslinux 的引导记录写到 U 盘的 MBR 上(这个操作貌似不会影响到存放在 U 盘里的文件,因为在硬盘上安装 GRUB 时也要写 MBR 到硬盘上)

  3. 挂载 Parted Magic 的 iso 文件,sudo mount -o loop,ro -t iso9660 pmagic.iso /media/cdrom,将 pmagic 目录以及 boot/syslinux/* 复制到 U 盘根目录

  4. 编辑 syslinux.cfg,修正里面的路径,把原本 /boot 的路径改为指向根目录。

  5. 安装 syslinux 到 U 盘:sudo syslinux /dev/sdb1

搞定。

(其实有更简单的工具,UNetbootin,是 Parted Magic 官网推荐使用的制作 Live USB 的方式)

Go 语言的错误处理机制

这段时间在学习 Go 语言,接触到一些比较“另类”的语言特性,其中一个就是它的错误处理机制,跟我以往所知的都不太一样。在我正儿八经地使用过的编程语言(C / Python / Ruby / JavaScript)里面,处理程序错误的方式大致有两种:1. 返回特殊值 2. 抛出异常。

C 语言属于第一种。函数调用出错时会返回特殊值,并有可能根据场景设置某个全局的(一般是 thread-local 的)错误代码。比如 printf 函数正常情况下返回的是已输出的字符数,如果出错,就返回负值。Linux (或其他遵循 POSIX 标准的 UNIX 系统) system calls 的函数传递错误信息的主要途径是设置全局的 errno 变量,这个变量保存的是当前线程最后一次发生的错误所对应的错误代码(详情见 man errno)。Windows API 中也有类似的函数,叫 GetLastError,也是 thread-local 的。

这种风格的错误处理方式可以看作是一种历史遗留的存在,一方面是由于 C 语言本身比较古老,在错误处理的语言特性上不会有太大革新;另一方面主流的操作系统(Linux / UNIX / Windows)提供的系统调用大量使用了这种方式,也不会有太大改变。此种风格的优点是语言设计者不需要在错误处理方面花太多心思,而缺点是函数返回错误所能携带的信息很有限,往往需要额外的文档解释。另外,大多数语言都不会强制程序员检查返回值,这些错误很容易被忽略,而每个函数调用都检查返回值的话会令代码看上去非常啰嗦(所以我们很少去检查 printf 是不是返回了负值)。

Python, Ruby 和 JavaScript 都提供了异常处理机制,属于第二种。异常处理包括两部分,一是抛出,二是捕捉。语言的运行时环境以及用户代码都有可能抛出预定义的或用户自行定义的异常,在 Python 和 Ruby 中以类实例的形式存在,在 JavaScript 中可以是任意值。异常被抛出后,当前代码会终止执行,而异常会传递给上层的函数调用者——上层如果不处理这个异常,就继续往上抛,直到虚拟机捕捉到异常并终止整个程序的执行为止。异常的捕捉可以在当前代码块或外层代码块(如上层函数)中进行,其效果是:1. 阻止该异常的传递(也支持重新抛出) 2. 继续异常发生点之后的代码的执行(Ruby 提供的 retry 语句更强大,会重新执行触发异常的代码)。

很多主流的编程语言都采用了这种风格,尽管某些细节上存在差异。其优点是程序在标示错误时能提供更丰富的上下文,包括运行时环境和函数调用堆栈等信息,方便程序调试。代码看上去也会简洁很多,除了显式捕捉一些特别重要的(如 Rails 里的 ActiveRecord::RecordInvalid)之外,其他不需要特殊处理的异常,要么在最顶层添加通用的处理(比如在 Web 应用中显示 500 页面),要么直接终止程序运行并打印 backtrace 信息(试试 python -m SimpleHTTPServer abc)。它的缺点没那么容易察觉,但也确实存在:程序中任何位置都有可能抛出异常,未捕捉的异常会终止程序的运行,而并不是所有程序都适合在顶层做通用的处理,所以写代码时总是要考虑两个问题:1. 我调用的代码会不会抛出异常 2. 我是否需要在当前代码中捕捉异常。对于大型项目的代码维护来说,这两个问题并没有那么容易确定,是个很大的思维负担(记住这一点,下文会有提及)。

初学 Go 语言时,以为它只是单纯继承了 C 语言的错误处理风格——在 Go 程序里到处都可以看到这样的代码:

f, err = os.Open(filename)
if err != nil {
    fmt.Println(err)
}

读了 golang.org 上两篇关于错误处理的文章之后,才知道 error 接口的存在(我还以为它只是个内置的数据类型),并明白了 panicrecover 的使用场景。

日常使用时,如果只是想返回一个简单的错误信息(比如 access denied),只需return errors.New("xxx: access denied"),稍微复杂一点的可以用 errors.Errorf输出格式化的字符串。这里有一个编码约定,即错误信息本身要标明错误发生的上下文,比如 os.Open 返回的错误信息会是 open /etc/passwd: permission denied 而不仅仅是 permission denied

在另外一些使用场景里,函数调用者会希望获取错误发生的更具体的信息,单靠字符串来传递这些信息显然不可行,替代的做法是定义一个新类型,实现它的 Error() 方法。这个方法会被 log.Fatal()fmt.Println 之类的函数调用,把它当成一个普通的字符串,而需要读取额外信息的调用者,则用 type assertion 将 error 类型转换为实际的错误类型(代码来自 Error Handling and Go):

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }

// decoding json data
if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

按照 Defer, Panic, and Recover这篇文章所说,Go 程序的一个惯例是对外的 API 尽量使用显式的 error 返回值,而内部错误处理可以根据需要使用 panicrecover 来简化流程。

关于两者的区别,Go 语言的核心开发者 Russ Cox 的解释是:在函数中如果出现了可预料的错误(比如网络连接失败,文件不存在,非法的 format 字符,等等),那就应该返回 error("if your function is in any way likely to fail, it should return an error")。如果是不可预料的(不应该出现的,“无法挽回的”)错误,比如数组越界(应该视作程序员的失误而不是程序运行环境的异常),那就是 panic. 按照 Go 的默认行为,"An unexpected panic like this is a serious bug in the program and by default kills it",而如果你想避免程序因为这种异常退出,可以用 recover 机制。

在函数内部调用 panic 会立即终止当前函数的执行,由当前调用栈逐层返回,一直到最顶层的 main 函数或是被某一层的 recover 捕捉。panic 可以接收值作为参数,而这个值会由 recover 返回给调用者。在这个语义上 panicrecover 跟传统的 try...catch 很相似,但它们有一个很刻意的限制:panic 被调用时会立即终止当前函数,开始执行由 defer 指定的清理函数,然后返回上一层调用栈,这意味着 recover 只有在 deferred function (不知道怎么翻译)中才能起作用,不能像 try...catch 那样在任意位置捕捉异常。

以上所述的机制与 C 语言的错误处理风格相比,只改进了错误信息传递这部分,而代码冗余以及容易忽略错误的问题却没有解决,网上吐槽这个问题的人不止我一个,在 Why I’m not leaving Python for Go 这篇文章中,作者 ubershmekel 说由于它没有“现代的”错误处理机制,他暂时不考虑使用 Go 语言("without modern error handling – I’m not going")。

Russ Cox 在 Google Plus 的一个 post 里回应了这篇文章。他解释了为什么 Go 要用函数返回值来表示错误而不是像目前的主流语言一样抛出异常。Russ Cox 说,在大型程序中判断是否要捕捉异常非常痛苦,影响开发效率(因为任何一个漏网的异常都有可能导致程序终止,降低了容错能力)。虽然 Go 语言的设计者也想让 Go 程序尽量简洁,但如果这种简洁是以增加大型程序维护成本为代价的话,就得不偿失了,这可以看作是语言设计上的一个折衷:

The error returns used by Go are admittedly inconvenient to callers, but they also make the possibility of the error explicit both in the program and in the type system. While simple programs might want to just print an error and exit in all cases, it is common for more sophisticated programs to react differently depending on where the error came from, in which case the try + catch approach is actually more verbose than explicit error results. It is true that your 10-line Python program is probably more verbose in Go. Go's primary target, however, is not 10-line programs.

这篇 post 消除了我一半的困惑,但是函数调用返回的错误容易被忽略这个问题要怎么解决呢?Russ Cox 没说,ubershmekel 说大概可以用代码静态分析检测出来,不知道是否可行,需要进一步的研究。

参考资料

在 Android 上使用 Gotunnel 翻墙

gotunnel 是 reus 同学用 Go 语言实现的一个 SOCKS5 代理,跟大众化的 OpenVPN 和 GoAgent 相比,gotunnel 的优势在于其私有协议没那么容易被 GFW “定点清除”(像 OpenVPN 最近就被封锁了无法连接,而 GoAgent 依赖的 Google App Engine 本身就很容易被墙)。跟它类似的代理服务器还有 shadowsocks,不过后者是用 Python 写的,占用资源较多,而且还依赖 Python 运行时环境。相反,由于 Go 程序是静态编译的,既省资源,又可以很方便地移植到不同的设备上,优势很明显。

这篇文章要讲的就是怎样在 Android 设备上使用 gotunnel 翻墙,测试的环境是已获得 root 权限的 Android 4.1 系统,涉及的操作包括交叉编译 gotunnel 和 redsocks,设置 iptables 转发规则以及解决 DNS 污染问题。事先声明:懒得折腾的同学,请考虑 GAE Proxy,SSH Tunnel,或者 VPN 这些更简易的方案。

编译 gotunnel

要编译 Android 版本的 gotunnel,有两种方法,一是在 Android 设备上安装 Go 开发环境,但目前官方下载页上面说 "no binary distribution for ARM yet",意味着这个方案不现实,遂放弃。替代的方法(其实是更正常的方法)是在桌面系统(Mac OS X / Linux / Windows)上交叉编译,指定目标系统为 ARM Linux。不过,Go 的二进制安装包并未包含交叉编译所需的工具链,需要自己编译。以下内容主要参考自 go-wiki 上的 Building windows go programs on linux 一文:

# 重新编译 Go 环境
cd $GOROOT/src && ./make.bash

# 这里只编译 ARM 架构的工具链
# 不同数字代表不同的 CPU 架构:5 - ARM, 8 - x86, 6 - amd64
for cmd in a c g l;
do
    go tool dist install -v cmd/5$cmd
done

# 安装 ARM Linux 工具链
export CGO_ENABLED=0
export GOARCH=arm
export GOOS=linux

go tool dist install -v pkg/runtime
go install -v -a std

安装完工具链,编译 gotunnel 就非常简单了(此处省去服务器部署的步骤):

git clone https://github.com/reusee/gotunnel
cd gotunnel/client

# 按自己的情况在 config.go 中设置服务器信息
CGO_ENABLED=0 GOARCH=arm GOOS=linux go build -o gotunnel

redsocks

Android 上的大多数软件都不支持单独设置代理,4.1 系统本身也没有相关的设置界面,唯一靠谱的途径是使用 redsocks + iptables,其数据传输流程大致为:app -> iptables -> redsocks -> gotunnel -> Internet. iptables 的作用是拦截所有对外的 TCP 请求,将这些请求转发到指定端口,这个端口由 redsocks 提供,并由它负责与 SOCKS 代理的客户端通信。

redsocks 同样需要交叉编译,其过程略复杂,想偷懒可以到 SSH Tunnel 主页 下载一个已经编译好的(顺便把 iptables 也下载下来,等会要用)。

想自己编译的话,步骤如下:

  1. 下载 Android NDK,利用 build/tools/make-standalone-toolchain.sh 安装工具链(我用的参数是 --toolchain=arm-linux-androideabi-4.6 --system=darwin-x86 --install-dir=/tmp/android-toolchain

  2. 下载 libevent,在这里下载 config.subconfig.guess,替换掉 libevent 原有的两个同名文件(否则 configure 脚本无法识别 Android 系统的工具链),编译并安装 libevent 到一个临时目录:

     # 请确认步骤 1 中的工具链的位置在 $PATH 中
     ./configure --host arm-linux-androideabi --prefix=/tmp/libevent
     make && make install
    
  3. redsocks 最新版本使用了 tsearch 函数,很悲剧的是 Android 工具链里没有提供这个库,解决方法:

    • 下载相关文件,放到 redsocks 目录里:search.h tfind.c tsearch.c tdelete.c tdestroy.c
    • redudp.c 中的 #include <search.h> 改为 #include "search.h"
    • 修改 Makefile,在 OBJS 这一行中添加 tsearch.o tdelete.o tdestroy.o tfind.o 修复之后可以正常编译:

      export CC=arm-linux-androideabi-gcc
      CFLAGS="-static -I/tmp/libevent/include" LDFLAGS="-L/tmp/libevent/lib" make
      

iptables

把前面编译的和下载的东西扔到 Android 上(可以安装 SSH Droid 后用 scp 传输,有 ssh 也方便进行 root 相关的操作),启动 gotunnel 和 redsocks 之后,添加 iptables 规则:

# 在 nat 表中新建一个名为 GOTUNNEL 的规则链
iptables -t nat -N GOTUNNEL
iptables -t nat -F GOTUNNEL

# 排除掉不需要走代理的 IP
iptables -t nat -A GOTUNNEL -d 192.168.0.0/16 -j RETURN

# 将其他 TCP 请求转发到 8118 端口(redsocks 监听的端口)
iptables -t nat -A GOTUNNEL -p tcp -j REDIRECT --to-ports 8118

# 启用 GOTUNNEL 规则链
iptables -t nat -A OUTPUT -p tcp -j GOTUNNEL

DNS

完成上述步骤后,仍然无法正常访问 Twitter 和 Facebook,因为 DNS 污染使得浏览器等应用无法解析到正确的 IP. (为什么 VPN 和 SSH 隧道没有这个问题呢?因为它们支持在远端解析 DNS,而 redsocks 不支持。)

解决方案:在 smarthosts 的基础上手动添加 Twitter 的 ip(我在 VPS 上逐个解析之后写到 hosts 里面)。有个叫 DNSLite 的应用提供了 DNS 代理,但没什么效果,我只用它来管理 hosts 文件。SSH Tunnel 内置的 DNS 代理效果比较好,如果能抽离出来作为单独的应用运行就好了。

THE END

至此大功告成!我的相关配置文件在这里:https://gist.github.com/4089326 (我用 Scripter 来管理启动和停止代理的脚本)

如何让 Mac 的 Ruby 使用 Libreadline

一开始我以为是终端的问题,irb 里很多常用的 bash 按键绑定都失效了,比如查找 输入历史的 C + r,后来才发现这是因为 Ruby 在 Mac 里面编译时默认使用了 libedit 而不是 libreadline (天知道为什么)。

Google 了一下,找到一个解法,步骤如下:

  1. 安装 6.0 版本的 readline 库:brew install readline

  2. 配置 Ruby 的 readline 扩展:

    # 请根据自己的 Ruby 版本调整目录名
    cd ~/.rvm/src/ruby-1.9.3-p194/ext/readline
    
    # 请根据 Homebrew 和 readline 库的安装目录调整路径
    ruby extconf.rb --with-readline-dir=/usr/local/Cellar/readline/6.0
    
  3. 确认第二步的命令输出里有 checking for readline/readline.h... yes 这一行,然后运行 make

  4. 运行 otool -l readline.bundle,确认输出的内容包含了 libreadline 而不是 libedit,然后运行 make install

搞定。来源: Getting ruby to use readline instead of libedit

Method Resolution Order in Python

Python 的类支持多继承,假如一个类 C 继承自 A 和 B,那么在调用它的实例方法时的 查找路径(Method Resolution Order,即 MRO)是怎样的呢?以前用 Python 的时候没有 思考过这个问题,因为当时几乎没有用过多继承,一直到在 Ruby 里大量使用 mixin 的 时候才对这个有所了解(Ruby 的 module mixin 可以看做是另一种形式的多继承)。

我在 Python 官网找到了 The Python 2.3 Method Resolution Order 这篇文章,作者很详尽地讲解了从 Python 2.3 开始使用的 C3 Method Resolution Order 方法(这个算法来自一篇叫 A Monotonic Superclass Linearization for Dylan 的论文)。

需要注意的是这个 C3 MRO 只对 new-style class 有效,classic class 的 MRO 仍然 保持从左至右(指的是声明类时指定的父类的顺序)的深度优先的查找方法。new-style class 的 MRO 之所以比 classic class 复杂,是因为在多继承的基础上所有的 new-style class 都派生自同一个父类 object,必须用稍微复杂一点的算法来确定一个线性的 继承路径。以下提到的类都属于 new-style class.

对于一个继承自 B1, B2, B3, ... BN 的类 C,它的线性继承关系 L(C) 是这样计算的:

L(C) = C + merge(L(B1), L(B2), L(B3), ... L(BN), [B1, B2, ... BN])

merge 方法的伪代码如下:

def merge(*args):
    results = []

    for current in args:
        for cls in current:
            if cls in tail_of_other_classes(args, current):
                break outer_loop
            else:
                results.append(cls)
                tail_of_other_classes(args, current).delete(cls)

        :outer_loop:

def tail_of_other_classes(all, current):
    return flatten(map(lambda xs: xs[1:], filter(lambda x: x != current, all)))

以下面这两个继承关系的 MRO 计算结果为例:

## example 1
O = object # L(O): O
class F(O): pass # L(F) = FO
class E(O): pass # L(E) = EO
class D(O): pass # L(D) = DO

# L(C) = C + merge(DO, FO, DF)
#      = C + D + merge(O, FO, F)
#      = C + D + F + merge(O, O)
#      = CDFO
class C(D, F): pass

# L(B) = B + merge(DO, EO, DE)
       = BDEO
class B(D, E): pass

# L(A) = A + merge(BDEO, CDFO, BC)
#      = A + B + merge(DEO, CDFO, C)
#      = A + B + C + merge(DEO, DFO)
#      = A + B + C + D + merge(EO, FO)
#      = ABCDEFO
class A(B, C): pass

## example 2
O = object
class F(O): pass
class E(O): pass
class D(O): pass
class C(D,F): pass

# L(B) = B + merge(EO, DO, ED)
#      = B + E + merge(O, DO, D)
#      = BEDO
class B(E,D): pass

# L(A) = A + merge(BEDO, CDFO, BC)
#      = A + B + merge(EDO, CDFO, C)
#      = A + B + E + merge(DO, CDFO, C)
#      = A + B + E + C + merge(DO, DFO)
#      = A + B + E + C + D + merge(O, FO)
#      = ABECDFO
class A(B,C): pass

这里还有一种例外情况:

O = object
class X(O): pass
class Y(O): pass
class A(X, Y): pass
class B(Y, X): pass

# the following declaration will raise error for Python >= 2.3
# TypeError: Error when calling the metaclass bases
#            Cannot create a consistent method resolution order (MRO) for bases object, X, Y
class C(A, B): pass

上面这个继承关系会触发异常,因为根据 C3 MRO 算法:

L(C) = C + merge(AXYO, BYXO, AB)
     = C + A + B + merge(XYO, YXO)

merge(XYO, YXO) 存在一个“死锁”,无法继续运算下去。

Updated on Sep. 3: 还有另外一种死锁的情况:

O = object
class B(O): pass
class C(B): pass

# the following declaration will raise error for Python >= 2.3
# TypeError: Error when calling the metaclass bases
             Cannot create a consistent method resolution order (MRO) for bases object, C, B
# L(N) = N + merge(BO, CBO, BC)
# both B and C block the calculation
class N(B, C): pass

# this works because:
# L(N) = N + merge(CBO, BO, CB)
#      = N + C + merge(BO, BO, B)
#      = NCBO
class N(C, B): pass

BTW: new-style class 有一个类方法 mro(),返回的就是 C3 MRO 计算的结果。