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 计算的结果。

关于 ActiveRecord 的唯一性检查

在 Rails 中如果要验证数据字段值的唯一性通常都会用到 validates_uniqueness_of 方法,比如我要确保用户注册时填写的 Email 地址是唯一的,就会这样写:

class User < ActiveRecord::Base
  validates_uniqueness_of :email
end

如果了解 validates_uniqueness_of 的实现细节的话就会知道,单纯这样写无法完全 保证 Email 值的唯一性,因为 ActiveRecord::Validations::UniquenessValidator 的 执行与随后写入数据到数据库这两个操作之间存在时间差,在多用户并发访问的情况下就 很容易写入重复的值。解决方法就是给 email 字段建一个 unique 的索引,把唯一性 检查交给数据库来做,这样如果插入重复值而 validates_uniqueness_of 又没有 检查到的话,就会抛 ActiveRecord::RecordNotUnique 异常。

其实这一点在 Rails 官方文档Concurrency and integrity 一节中有详细的讲解,我要说的是另一个问题,那就是既然 validates_uniqueness_of 不可靠,为什么不省掉这一步,直接捕捉(可能会抛出的)ActiveRecord::RecordNotUnique 异常?

前几天在 Ruby China 问了这个问题(链接), 根据大家的回复总结出的答案就是“方便 form validation error messages 的显示”, 因为 ActiveRecord::RecordNotUnique 这个异常没有包含太多信息(比如到底是哪个 字段重复了),无法给予用户必要的提示。

所以结论是 validates_uniqueness_of 要写,而 ActiveRecord::RecordNotUnique 这个异常也要在 controller 里面捕捉并适当处理,比如这样(没有实际试验过):

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotUnique, with: :recheck_uniqueness

  private
    def recheck_uniqueness
      # trigger validations again so that duplication can be detected
      @user.valid?
    end
end

New to Mac

6 月底拿到了公司配的 MacBook Pro,2011 Early 款的,虽然配置跟新出的 rMBP 完全 没法比,但相对于我原先的 ASUS 笔记本来说已经是很大的提升了。这里说说我作为一个 使用不到两周的 Mac 新手的经验和体会。

包管理工具

Linux 的发行版大多都自带一个包管理工具,日常使用的软件都可以通过它来安装,而 Mac OS 默认是没有的,必须自己选择和安装。我总共试用了 Gentoo Prefix, MacPorts 和 Homebrew 这三个,最后选择的是 Homebrew.

我是 Gentoo 党,本来还想着在这部 MBP 上装 Gentoo Linux,但考虑到这是工作机 不方便折腾就作罢了,听闻 Gentoo Prefix 还不错,就花了一天的时间把它配置好(虽然 有官方文档但还是会有一些小问题需要自己 Google)(其过程几乎是一次 LFS), 发现它根本没有针对 Mac OS 作任何优化或调整,很多包都用不了(比如 htop),最后很 不情愿地把它删了。

接着试用了 MacPorts,但安装完没多久就放弃了,因为它跟 Gentoo Prefix 一样,整个 工具链都要从头开始编译,比如其中一个包依赖 Python,就要自己编译一个 Python,而 不是利用系统现有的,没觉得这样做有什么优势。

Homebrew 算是一个简化版的 ports 系统,用 Git 来管理软件包的 formula(类似 Gentoo Portage 中的 ebuild 文件),安装软件的时候会尽量利用系统已有的工具,虽然可用的 包相对于 MacPorts 要少很多,但估计也够用了。等碰到 Homebrew 解决不了的问题时再 考虑其他的方案吧。

BTW:Mac OS 的开发工具链(gcc 之类的东西)是由 Xcode 里的 Command Line Tools 提供的,如果嫌 Xcode 体积太庞大可以下载单独的安装包,不过我建议还是安装整个 Xcode 比较好,因为不确定那些依赖 Cocoa 的程序没有 Xcode 能否正常编译,而且 Xcode 还自带了一些比较好用的工具(比如 FileMerge)。

开发环境

选好包管理工具之后剩下的环境搭建比较简单,浏览器用 Google Chrome,终端模拟器用 iTerm2,编辑器用 MacVim,都是一些“大路货”,但也有一些小工具需要自己慢慢发掘, 这里推荐几个我觉得很有用的:

  • SizeUp: 窗口管理工具,提供了一些简易的窗口平铺选项,以及很关键的窗口最大化 功能——大多数 Linux 用户都会很惊讶地发现在 Mac OS 系统里面,窗口 左上角那个绿色按钮并不是最大化按钮,而系统没有提供最大化的功能……(比较可惜的 是在 App Store 买不到,只好厚脸皮地无限期试用着)

  • Dash: 各种开发文档的离线包,虽然做工不是太精细(估计是直接在官网抓下来然后打包的), 但也省了自己一个个用 wget 抓的功夫。

  • ClipMenu: 剪贴板管理工具,这个跟我在 Linux 里用的 Parcellite 比较接近。

  • Alfred: 类似 Gnome Do 的东西(好吧它们都是源自 Mac OS 里的 QuickSilver), 免费版功能比较简单,据说装了 Powerpack 之后会很强大,但是在 App Store 里买不到……

  • DragonDrop: 用 Trackpad 长距离拖动东西比较考验用户的技术,而 DragonDrop 提供了“中转站”的功能,可以把要拖动的东西放进去,到目的地再拖出来。App Store 有售。

  • Scroll Reverser: 自 Lion 开始 Trackpad 的滚动方向是与触摸屏的“自然方向”一致的, 但是鼠标滚轮和 Trackpad 的手感完全不同,还是保持“传统方向”比较顺手,而这个工具就 可以让你分开设置 Trackpad 和鼠标滚轮的滚动方向。

  • Tunnelblick: OpenVPN 的图形客户端。不知道为什么通过 Homebrew 安装的 OpenVPN 命令行版无法正常连接, Google 了半天也没找到解决方法——大家都说“用 Tunnelblick 吧”,所以别折腾了,用 Tunnelblick 吧。

  • TotalFinder: Finder 强化工具,提供多标签视图、双栏视图和文件剪切等功能。是的, Finder 默认没有文件剪切选项。

一些注意事项

  • 通过 brew install macvim 安装 MacVim 的时候,有可能会在编译过程中卡住 不动,据说是因为找不到 Xcode 的位置,所以要先运行 sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer 再重新安装。

  • Mac OS 自带的命令行工具都是 BSD 系的,不仅很多参数不支持,其参数的风格也跟 Linux 里的 GNU 系工具不太一样,比如 rm 命令的 -rf 必须写在最 前面(rm -rf xxx 可以,rm xxx -rf 就报错),又比如 ls 命令没有 --auto 选项。如果不介意的话可以慢慢适应,但也有比较暴力的解决方法,就是先 brew install coreutils 然后按照 Homebrew 的提示用 GNU Coreutils 替换掉系统自带的工具。

一些吐槽和赞美

Mac OS 图形系统的整合程度比较高,虽然我不清楚具体的架构,但估计跟 Windows 的做法差不多, 这就意味着选择了 Mac OS 就必须适应(或者说“忍受”)它自带的桌面环境,而这对于一个 Awesome WM 的死忠来说并不是一个好消息,且不说 SizeUp 之类的工具提供的窗口平铺 功能太弱(而且还是收费软件),像“将窗口放置到指定的 Desktop”这种简单的操作 都要用鼠标拖动才能完成,简直是不可想象的。另外据说 Mac OS 对多显示器的支持也很 糟糕,搞得我都不怎么想买外置显示器了……

不过呢,赞美还是有的:

  1. MBP 的休眠功能非常靠谱,出门时只需要合上屏幕就可以带走,而拿出来打开屏幕又立即可以使用,不像在 Linux 里面休眠和唤起都要半天才有反应,还经常在休眠过程中死机(有可能是笔记本硬件的问题)。

  2. Mac OS 里的好用的软件比 Linux 多很多,常用的中文输入法、QQ、Evernote 等等都有 Mac 版。

  3. Cocoa 原生程序的文本编辑框都支持 Emacs 按键绑定(C-a / C-e 移动到行首行尾, C-k / C-y 剪切粘贴文本,C-p / C-n 移动到上一行下一行,等等),比 PC 键盘的 Home / End 按键高效很多。

Rails Nested Layout

一个 Rails 项目里经常会有多个不同的 layout,比如网站的前台显示、后台管理和 登录注册页面各自使用不同的布局设计,而这些 layout 文件在代码级别又会存在一些 共有的代码,像是 <head></head> 里的 meta tags,或者是 HTML5 Boilerplate 这类框架里用到的 conditional comments, 如果每个文件都重复一遍这些代码的话,不仅不方便维护,代码看上去也没那么清晰。

按照 Rails 惯用的做法,可以把这些代码以 partial 的形式封装起来,然后在 每个 layout 里面 render,算是一个比较简单的解决方案,适合 meta tags 这类可以直接 插入的代码片段。但是对于 conditional comments 这种需要提供 nesting block 的,就 不管用了,需要使用一些更“高级”的方法。

第一种方法是写成 helper method,利用 capture 方法将传给 helper 的 block 转换为字符串,把它跟需要插入的代码拼接起来(使用 string interpolation 或 content_tag)。比如这样:

# in helper
def insert_conditional_comments(&block)
  base = "<!--[if lt IE 7]> <html class='no-js lt-ie9 lt-ie8 lt-ie7' lang='zh-cn'> <![endif]--><!--[if IE 7]> <html class='no-js lt-ie9 lt-ie8 ie7' lang='zh-cn'> <![endif]--><!--[if IE 8]> <html class='no-js lt-ie9 ie8' lang='zh-cn'> <![endif]--><!--[if gt IE 8]><!--> %s <!--<![endif]-->"

  html = base % (capture { block.call })
  html
end
# in views
= insert_conditional_comments do
  %p some html or text here

另外一种方法跟 Django 框架的模板语法(extendblock)有点类似(当然内部 实现应该是完全不同的),先调用 content_for 把 sub layout 的内容暂存起来,然后 手动调用 render template: 'parent_layout',而 parent layout 中再通过 yield 把 sub layout 的内容插入到指定位置。比如这样:

# layouts/base.html.haml

!!!
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="zh-cn"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8 ie7" lang="zh-cn"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9 ie8" lang="zh-cn"> <![endif]-->
<!--[if gt IE 8]><!-->
%html.no-js{ :lang => 'zh-cn' }
  <!--<![endif]-->
  %head
    %meta{ :charset => 'utf-8' }
    %meta{ 'http-equiv' => 'X-UA-Compatible', :content => 'IE=edge,chrome=1' }
    = csrf_meta_tags

  %body
    = content_for?(:body) ? yield(:body) : yield
    = render 'shared/google_analytics'
# layouts/frontend.html.haml

- content_for :body do
  #wrapper
    #main= yield

= render :template => 'layouts/base'

注意上面的代码,在 base.html.haml 里我没有直接写 yield :body,而是先判断 content_for :body 有没有内容,如果没有的话就 yield——这样做的好处是 base.html.haml 自己也可以当成一个普通的 layout 来用。

通常情况下,两级的 nesting 就基本够用了,不过按照 Rails 官方文档的说法, nesting 是没有层级限制的,这一点跟 Django 也比较相似。(如果想在 Rails 里用 Django 的模板语法的话,可以试试 Liquid)。

参考:Layouts and Rendering in Rails

转到 Gentoo 一个月了

之前用的是 Arch,再之前是 Ubuntu 10.10。选 Gentoo 是因为我觉得其它发行版都大同小异,只有 Gentoo 比较特别一点。

机器配置:CPU Pentium Dual-Core T4200 @ 2.00GHz;RAM 2G;HDD 256G

安装时间:编译时间比较长的是 kernel,xorg-server,和 KDE 的一堆依赖,不过首次安装时主要花的时间并不是编译(一次全部编译完估计也不到12小时),而是查文档和写配置文件。我花了四天的时间把整个系统配置好。

引导介质:我用的是 Arch Linux 的 net install iso,机器配置好一点的话可以考虑 Ubuntu 的 Live CD(图形界面方便上网查资料什么的,不然就要另外准备一部机器)。

内核配置:内核配置其实很简单,忽略掉所有 experimental 和 deprecated 的选项,选上 Gentoo 官方文档推荐的,再根据自己硬件配置勾上必要的驱动就好——要注意的是每次编译新内核都要保留上一个作为 fallback,等确认新内核正常工作后再删掉旧的。

USE flags:这个要仔细看文档,按照自己的需要来配置。全局的 USE flags 不要经常改,不然 emerge world 会很疼的。

Overlay: 有些包的官方 ebuild 并不一定符合自己的需要,而且单靠修改 USE flags 不一定有效,可以试着写一个 overlay(具体方法看官方文档)。比如我装 KDE 的时候就砍掉了很多不需要的包(比如那个坑爹的桌面搜索)(估计减少了50%的编译时间)。

桌面环境:X Window 的配置跟 Arch 差不多,只要显卡驱动正常加载就不会有问题。桌面环境我用的是 KDE + Awesome 的混搭,GNOME 相关的依赖几乎一个都没有装……

大型软件:Firefox / Google Chrome / VirtualBox 这几个比较大的软件还是直接用官方的二进制包吧,自己编译没啥好处而且没一两个小时都编译不完。

日常使用:Gentoo 并没有比 Arch 快多少,毕竟硬件没有升级,整体感觉倒是舒服了很多。

总结:Gentoo 也只是一个*正常的*发行版而已,只不过首次安装要稍微花多一点时间。推荐有 Arch 使用经验的人安装。

介绍两个新工具

Guake

Guake 是一个“下拉式终端模拟器”,特点是可以用热键呼出窗口,不需要的时候又可以将其 隐藏——本质上它跟我之前用的 Terminator 没有什么不同(Terminator 也可以设置 热键呼出),对于 Awesome 党来说无非是 Win+1 (Awesome 里切换到 Desktop 1 的热键) 和 Win+` 的区别而已……不过它更轻量,也更稳定(Terminator 经常假死),所以还是值得一试的。

有一个小问题需要自己改源码解决:光标形状。Guake 里默认所有光标的形状都是方块形( block)的,在 Vim 里看着很不方便。Google 到的解决方案是修改 /usr/lib/guake/guake.py 里的 GuakeTerminal 类:

# guake.py 第 433 行
class GuakeTerminal(vte.Terminal):
    """Just a vte.Terminal with some properties already set.
    """
    def __init__(self):
        super(GuakeTerminal, self).__init__()
        self.configure_terminal()
        # 加入这一行,TerminalCursorShape 的参数请自行试验
        self.set_cursor_shape(vte.TerminalCursorShape(1))
        # 省略 N 行代码...

修改完重启 Guake,光标的形状就会变成竖线形(beam)的啦。

tmux

tmux 可以看作是增强版的 screen,在窗口操作和配置等方面都比 screen 顺手。很早就听 说过它的强大,只是之前一直在用 Terminator,有多标签页和分屏等功能,就懒得去配置 tmux. 这次从 Terminator 切换到 Guake,发现没了分屏功能真的很不习惯,而且用 Ctrl+Page Up 或鼠标来切换标签也太低效了(而且 Guake 的标签 UI 好丑 = =),就给了 tmux 一次机会, 只用了不到半小时就爱不释手了~

利用 tmux 的按键绑定,可以减少很多重复操作,比如我在写 Rails 程序的时候需要同时 打开 unicorn, tail -f development.log, tail -f unicorn.stderr.log, compass watchrails c,在 tmux 里就可以这样设置:

# 分屏显示 log
bind L neww -n log -t 4 'tail -f log/unicorn.stderr.log' \; \
       splitw 'tail -f log/development.log' \; \
       resizep -U 10 \; selectp -U \; splitw -h \; \
       send 'unidev && bundle exec compass watch' 'C-m'

# 启动 rails console
bind R neww -n repl -t 3 \; send 'rails c' 'C-m'

只要在 Rails 项目的目录里按 C-b L,就可以同时打开所需的 log,效果图:

有兴趣可以看看我的 tmux.conf