2
19
2014
18

利用 Aufs 和 LXC 快速建立一个用于测试的系统副本

起因是,我偶尔看到 MediaWiki 导出时可以把图片也包含在 XML 文件中,但是不确定能不能顺利地导入回去。本来是准备拿虚拟机测试的,但是得在虚拟机里安装整套环境,麻烦呀。于是,结合前段时间折腾 Aufs 和 LXC 的经验,把当前正在运行的系统利用 Aufs 搞了一份只读挂载。当然还要弄个空目录来放可写分支:

mkdir -p root data
sudo mount -t aufs -o br:$PWD/data=rw:/=ro aufs $PWD/root

其实这个样子就已经可以 chroot 进去跑 httpd 了。不过,得先改一下监听的端口,因为 chroot 环境与主系统只有文件系统是隔离的,网络空间还是共享的。chroot 中 PID 空间也是共享的,所以在里边杀进程时不小心把 PID 写错的话,是可能会把外边的进程给杀掉的……(而 LXC 中,主系统是可以杀容器中的进程,但是反过来不行,因为主系统中的进程在容器中根本没分配 PID。)

于是就来玩玩 LXC 啦。要注意把 fstab 删掉,不然 systemd 会不高兴。日志文件不能共享,否则 journald 会不高兴。因为把 mknod 权限给禁掉了,所以在容器里 loop 设备是没法创建的。如果需要,在主系统里 losetup 之后像注释里那样写一条挂载信息就好。

sudo rm root/etc/fstab
sudo rm -r root/var/log/journal
sudo mkdir root/var/log/journal
sudo chgrp systemd-journal root/var/log/journal
sudo brctl addbr br0
sudo ifconfig br0 192.168.10.1

cat > lxc.conf <<EOF
lxc.utsname = arch2
lxc.autodev = 1
lxc.tty = 1
lxc.pts = 1024
lxc.rootfs = ${PWD}/root
lxc.mount.entry = sysfs sys sysfs ro,defaults 0 0
lxc.mount.entry = proc proc proc nodev,noexec,nosuid 0 0
lxc.mount.entry = /proc/sys ${PWD}/root/proc/sys none ro,bind 0 0
lxc.cap.drop = mknod sys_module mac_admin mac_override
# loop mount
# lxc.mount.entry = /dev/loop1 /home/lilydjwg/tmpfs/root/var/lib/pacman ext4 rw 0 0
#networking
lxc.network.type = veth
lxc.network.link = br0
lxc.network.flags = up
lxc.network.ipv4 = 192.168.10.3
lxc.network.name = eth0
#cgroups
lxc.cgroup.devices.deny = a
lxc.cgroup.devices.allow = c *:* m
lxc.cgroup.devices.allow = b *:* m
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
lxc.cgroup.devices.allow = c 1:7 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 4:1 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:2 rwm
lxc.cgroup.devices.allow = c 136:* rwm
EOF
sudo lxc-start -n arch-dup -f lxc.conf

当然网络和 DNS 还要进去再设置一下:

route del -net 192.0.0.0/8
route add -net 192.168.0.0/16 eth0
route add -net default gw 192.168.10.1
echo 'nameserver 192.168.10.1' > /etc/resolve.conf

LXC 挺有点复杂的。systemd 的开发者也是这么认为的,所以他们搞了个操作便捷性类似于 chroot 但是功能类似于 LXC 的东东——systemd-nspawn!比如上边那个新系统可以这么启动:

sudo systemd-nspawn -b --private-network -D root

不过很遗憾的是,要么加--private-network让新启动的容器没有网络,要么不加,和 chroot 一样与主系统共享网络。毕竟是他们用来测试 systemd 的东东嘛。调试系统的第一个进程可不容易,但是当它在另一个系统中只是一个普通进程、可以连 gdb 和 strace 时情况就大不一样啦 =w=

PS: 在 systemd-nspawn 的 manpage 中(上边那个 freedesktop.org 的链接),Arch 和 Fedora 以及 Debian 并列作为示例了呢 =w=


2015年3月14日更新:使用 Linux 3.18 及以上版本的内核,也可以使用 overlayfs 取代 aufs 来挂载,挂载命令示例如下:

modprobe overlay
mount -t overlay -o lowerdir=/,upperdir=$PWD/.lxc-data,workdir=$PWD/.lxc-root overlayfs $PWD/.lxc-root

lowerdir是只读的目录(其中的数据不会被修改),upperdir是用于记录修改的可写目录,workdir是工作目录,其必要性我也不理解,需要和upperdir同一文件系统。我习惯上指定为挂载目标目录。

overlayfs 某些操作的效率似乎比 aufs 高不少。这里是我自己用来创建这个系统副本的 Shell 脚本。

Category: Linux | Tags: linux systemd lxc aufs
2
19
2014
3

zsh 异步生成提示符

为什么要异步?当然是因为慢了。比如 Arch 核心仓库 git 版挺大的,第一次进去时显示个 git 分支名要等好一会儿。今天在 zsh-users 列表中看到 Bart Schaefer 给出了一个使用 coprocess 的解决方案,眼前一亮,立即照葫芦画瓢给自己的 zsh 用上了。以下是整个提示符设置部分的代码:

if [[ -n $commands[git] ]]; then
  _nogit_dir=()
  for p in $nogit_dir; do
    [[ -d $p ]] && _nogit_dir+=$(realpath $p)
  done
  unset p

  typeset -g _current_branch= vcs_info_fd=
  zmodload zsh/zselect 2>/dev/null

  _vcs_update_info () {
    eval $(read -rE -u$1)
    zle -F $1
    exec {1}>&-
    zle reset-prompt
  }

  _set_current_branch () {
    cwd=$(pwd -P)
    for p in $_nogit_dir; do
      if [[ $cwd == $p* ]]; then
        return
      fi
    done

    setopt localoptions no_monitor
    coproc {
      _br=$(git branch --no-color 2>/dev/null)
      if [[ $? -eq 0 ]]; then
        _current_branch=$(echo $_br|awk '{if($1 == "*"){print "%{\x1b[33m%} (" substr($0, 3) ")"}}')
      fi
      # always gives something for reading, or _vcs_update_info won't be
      # called, fd not closed
      typeset -p _current_branch
    }
    disown %{\ _br
    exec {vcs_info_fd}<&p
    # wait 0.1 seconds before showing up to avoid unnecessary double update
    # precmd functions are called *after* prompt is expanded, and we can't call
    # zle reset-prompt outside zle, so turn to zselect
    zselect -r -t 10 $vcs_info_fd 2>/dev/null
    zle -F $vcs_info_fd _vcs_update_info
  }

  typeset -gaU precmd_functions
  precmd_functions+=_set_current_branch
  setopt PROMPT_SUBST
fi

[[ -n $ZSH_PS_HOST && $ZSH_PS_HOST != \(*\)\  ]] && ZSH_PS_HOST="($ZSH_PS_HOST) "

E=$'\x1b'
PS1="%{${E}[2m%}%h $ZSH_PS_HOST%(?..%{${E}[1;31m%}%?%{${E}[0m%} )%{${E}[32m%}%~\$_current_branch
%(!.%{${E}[0;31m%}###.%{${E}[1;34m%}>>>)%{${E}[0m%} "

比较坑的是使用chpwd_functions的话只能在目录改变时显示一次,再随便执行个什么命令分支提示就没了。又想到目录不改变的时候分支也可以变化(切换分支了嘛),所以使用precmd_functions,每次显示提示符前(单纯的重绘除外)都执行一次。另外,为了避免每次显示提示符时都明显地分为两步干扰视线,所以在那个_set_current_branch函数里等了 0.1 秒,超时才会不管分支名显示先继续了。

2014年2月24日更新:注意,直到 zsh 5.0.5(就是当前最新版本)有个 bug,在显示提示符之后、用户输入之前,上述代码会经常出现「忙等待」的情况浪费 CPU。这里有个补丁可以修复。

Category: shell | Tags: linux Git zsh
2
7
2014
27

Linux「真」全局 HTTP 代理方案

看到 ArchWiki 上 GoAgent 条目的亚全局代理方案,只是设置了代理相关环境变量。我就想,为什么不实现一个真正的全局 HTTP 代理呢?

最终,答案是:Linux 太灵活了,以至于想写一个脚本来搞定很麻烦。不过方案如下,有兴趣的可以折腾折腾。

首先,需要用到的工具:dnsmasq、iptables、redsocks,以及 HTTP 代理工具。dnsmasq 是用来缓存 DNS 请求的,iptables 把 TCP 流转接到 redsocks,而 redsocks 将 TCP 流转接到代理上。

最小 dnsmasq 配置如下:

listen-address=127.0.0.1
cache-size=500
server=127.0.0.1#5353
bogus-nxdomain=127.0.0.1

这里使用了本地的 dnscrypt 服务(假设其在 5353 端口上提供服务)。也可以使用国外服务器,只是需要更细致的配置来迫使其走 TCP。

iptables 命令如下:

# 创建一个叫 REDSOCKS 的链,查看和删除的时候方便
iptables -t nat -N REDSOCKS
# 所有输出的数据都使用此链
iptables -t nat -A OUTPUT -j REDSOCKS

# 代理自己不要再被重定向,按自己的需求调整/添加。一定不要弄错,否则会造成死循环的
iptables -t nat -I REDSOCKS -m owner --uid-owner redsocks -j RETURN
iptables -t nat -I REDSOCKS -m owner --uid-owner goagent -j RETURN
iptables -t nat -I REDSOCKS -m owner --uid-owner dnscrypt -j RETURN

# 局域网不要代理
iptables -t nat -A REDSOCKS -d 0.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A REDSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A REDSOCKS -d 240.0.0.0/4 -j RETURN

# HTTP 和 HTTPS 转到 redsocks
iptables -t nat -A REDSOCKS -p tcp --dport 80 -j REDIRECT --to-ports $HTTP_PORT
iptables -t nat -A REDSOCKS -p tcp --dport 443 -j REDIRECT --to-ports $HTTPS_PORT
# 如果使用国外代理的话,走 UDP 的 DNS 请求转到 redsocks,redsocks 会让其使用 TCP 重试
iptables -t nat -A REDSOCKS -p udp --dport 53 -j REDIRECT --to-ports $DNS_PORT
# 如果走 TCP 的 DNS 请求也需要代理的话,使用下边这句。一般不需要
iptables -t nat -A REDSOCKS -p tcp --dport 53 -j REDIRECT --to-ports $HTTPS_PORT

redsocks 的配置:

base {
  log_debug = off;
  log_info = off;
  daemon = on; 
  redirector = iptables;
}
// 处理 HTTP 请求
redsocks {
  local_ip = 127.0.0.1;
  local_port = $HTTP_PORT;
  ip = $HTTP_PROXY_IP;
  port = $HTTP_PROXY_PORT;
  type = http-relay; 
}
// 处理 HTTPS 请求,需要一个支持 HTTP CONNECT 的代理服务器,或者 socks 代理服务器
redsocks {
  local_ip = 127.0.0.1;
  local_port = $HTTPS_PORT;
  ip = $SSL_PROXY_IP;
  port = $SSL_PROXY_PORT;
  type = http-connect;  // or socks4, socks5
}
// 回应 UDP DNS 请求,告诉其需要使用 TCP 协议重试
dnstc {
  local_ip = 127.0.0.1;
  local_port = $DNS_PORT;
}

然后以相应的用户和配置文件启动 dnsmasq 以及 redsocks。修改/etc/resolv.conf

nameserver 127.0.0.1

至于分流的事情,HTTP 部分可以交给 privoxy,但是 HTTPS 部分不好办。可以再设立一个像 GoAgent 那样的中间人型 HTTPS 代理,或者更简单地,直接根据 IP 地址,国内的直接RETURN掉。

以上就是整个方案了。有些麻烦而我又不需要所以没测试。反正就是这个意思。Android 软件 GAEProxy 就是这么干的(不过它没使用 iptables 的 owner 模块,导致我不小心弄出了死循环)。另外,BSD 系统也可以使用类似的方案。

2
2
2014
17

grub2 引导 Arch Linux 安装镜像的方法

志以备链。

一、准备设备

U 盘、SD 卡、硬盘、移动硬盘均可。不需要重新分区,但是引导部分将被覆盖。不要用太奇葩的分区格式,Windows 和 Linux 下常见的都行。要有地方装 grub。grub 一般情况是安装在第一个分区前的空闲空间(MBR 分区表)或者专门准备的一个分区(GPT 分区表,参见 ArchWiki)。把下回来的镜像文件(如archlinux-2014.01.05-dual.iso)扔进去。注意据说 BIOS 只能读取 USB 设备前 8G 的内容,因此如果是移动硬盘,最好把文件放第一个分区中。

二、安装 grub2 到设备上(如果目标设备已经安装则跳过)

先挂载镜像所在分区,再执行安装命令。注意:老旧系统可能需要使用grub2-install命令。

sudo mount /dev/sdb1 /mnt
sudo grub-install /dev/sdb --boot-directory=/mnt

三、配置

默认没有配置文件,我这里给出一个:

set UUID=xxxxxxx
set ver=2014.01.05
set label=ARCH_201401
set isofile="/archlinux-$ver-dual.iso"

insmod part_gpt
insmod part_msdos
if [ -s $prefix/grubenv ]; then
  load_env
fi
if [ "${next_entry}" ] ; then
  set default="${next_entry}"
  set next_entry=
  save_env next_entry
  set boot_once=true
else
  set default="0"
fi

if [ x"${feature_menuentry_id}" = xy ]; then
  menuentry_id_option="--id"
else
  menuentry_id_option=""
fi

export menuentry_id_option

if [ "${prev_saved_entry}" ]; then
  set saved_entry="${prev_saved_entry}"
  save_env saved_entry
  set prev_saved_entry=
  save_env prev_saved_entry
  set boot_once=true
fi

function savedefault {
if [ -z "${boot_once}" ]; then
  saved_entry="${chosen}"
  save_env saved_entry
fi
}

function load_video {
if [ x$feature_all_video_module = xy ]; then
  insmod all_video
else
  insmod efi_gop
  insmod efi_uga
  insmod ieee1275_fb
  insmod vbe
  insmod vga
  insmod video_bochs
  insmod video_cirrus
fi
}

set menu_color_normal=light-blue/black
set menu_color_highlight=light-cyan/blue

if [ x$feature_default_font_path = xy ] ; then
  font=unicode
else
  insmod part_msdos
  insmod ext2
  # 填入自己那个分区的 UUID
  search --no-floppy --fs-uuid --set=root $UUID
  font="/grub/fonts/unicode.pf2"
fi

if loadfont $font ; then
  set gfxmode=auto
  load_video
  insmod gfxterm
  set locale_dir=$prefix/locale
  set lang=zh_CN
  insmod gettext
fi
terminal_input console
terminal_output gfxterm
set timeout=500

menuentry "Archlinux Live ISO $ver (x86_64)" {
  loopback loop $isofile
  linux (loop)/arch/boot/x86_64/vmlinuz archisolabel=$label img_dev=/dev/disk/by-uuid/$UUID img_loop=$isofile earlymodules=loop
  initrd (loop)/arch/boot/x86_64/archiso.img
}

注意前几行。把UUID设置成自己 grub 安装的那个分区的 UUID。UUID 可以使用命令lsblk -o +UUID取得。ver设置成自己所使用的安装镜像中间的日期部分,label那里有相应的年份和月份,相应地修改一下。然后保存为/mnt/grub/grub.cfg就可以了。isofile指向自己放 ISO 文件的地方,路径也是相对于挂载点的。

四、外部链接

  1. 雪月秋水的使用GRUB2引导ISO镜像
Category: Linux | Tags: grub grub2 Arch Linux
2
2
2014
9

玩转 systemd 之用户级服务管理

前几日群里又有人提到使用 systemd 管理用户的守护进程。早些时候我就知道有设计这么个功能,然调用systemctl --user时却总是跑去执行/bin/false,然后告诉我执行失败 QAQ

既然提到,就再试试呗。没想到这次systemctl --user列出一大堆 unit!还没弄明白它是哪里来的,不过至少说明systemctl --user已经可以用了!于是尝试性地把几个不依赖 X 的服务改成 systemd 服务了。至于依赖 X 的就不要 systemd 管了,我自己写在 Awesome 配置里,毕竟它们属于这个 X 会话,独立性没那么强,也免得纠结 DISPLAY 环境变量的问题。

我的 systemd 版本是 208。大概以前的版本确实有些问题,现在已经弄好了。

systemd 的用户配置位于~/.config/systemd/user/lib/systemd/user/etc/systemd/user下。我当然是用「家」里的那个了。

我改写的这些服务之前大部分是在 tmux 里跑的,除了占用 tmux 窗口外还占用个 zsh。现在全部改写成 systemd 服务了,于是 tmux 那边干净了不少 =w=

比如这个:

[Unit]
Description=Privoxy Web Proxy With Advanced Filtering Capabilities
ConditionPathIsDirectory=%h/.privoxy

[Service]
Type=simple
PIDFile=/run/user/%U/privoxy.pid
SyslogFacility=local0
ExecStart=/usr/bin/privoxy --pidfile /run/user/%U/privoxy.pid --no-daemon %h/.privoxy/config

[Install]
WantedBy=default.target

可以看到,systemd 里写起要执行什么命令还是挺简陋的。毕竟它不调用 shell 的,于是环境变量都用不了,家目录的位置也要写成%h%U则是用户 ID。令我不解的是,执行的命令和#!里的一样,必须使用绝对路径,而不能只写个名字,依赖 $PATH 环境变量去寻找。于是就有这样丑陋的ExecStart了——

ExecStart=/bin/zsh -c "exec sslocal -s HOST -p PORT -l LOCALPORT -k PASSWORD -m METHOD -c <(echo {})"

至于那个SyslogFacility,是为了在 syslog-ng 的日志中不显示出来而定义的,这样就可以根据 facility 来过滤掉这些用户级服务的日志了。

还有个SyslogIdentifier也挺好用的。systemd 默认使用ExecStart里第一个字的文件名部分来作为日志的标识,于是在系统日志(syslog 和 journald)中就看到一堆 sh 以及一些 zsh 和 socat,没区分度了……于是写明 SyslogFacility 来赋予其一个更合理的名字。

systemd 用户级服务使用systemctl --user来管理,其它的和系统级的差不多,也是 enable / start / disable / stop / status 这些。当然 daemon-reload 命令会频繁地使用啦 :-)

查看 journald 日志却有些不同,也是加--user,但是查看某个服务的日志输出却不是-u SERVICE了。有个--user-unit NAME参数,但是它会只显示 systemd 启动的进程的日志,而没有 systemd 自己处理该服务时的日志了……通过阅读日志的 JSON 格式输出(-o json-pretty),终于弄了个比较理想的方案。以下是我在 zshrc 里的相关配置(zsh only 哦):

alias sysuser="systemctl --user"
function juser () {
  # sadly, this won't have nice completion
  typeset -a args
  integer nextIsService=0
  for i; do
    if [[ $i == -u ]]; then
      nextIsService=1
      args=($args _SYSTEMD_CGROUP=/user.slice/user-$UID.slice/user@$UID.service/)
    else
      if [[ $nextIsService -eq 1 ]]; then
        nextIsService=0
        args[$#args]="${args[$#args]}$i.service"
      else
        args=($args $i)
      fi
    fi
  done
  journalctl -n --user ${^args}
}

-n是因为默认全部输出,一下子跳到后边的日志时就太卡了,但是只输出最后一部分日志就挺快的。默认是十行,也可以接数字来指定数量。指定多次时最后一次生效,所以我问题把它写进去也不怎么影响手动指定的情况。

于是管理用户级服务时我就用sysuser ...,而要看某个服务的日志时就用juser -u xxx

因为我使用 eCryptfs 加密了主目录,所以还遇到一个问题:用户级的 systemd 进程是通过 PAM 启动的,而这启动的时候我的主目录还没解密呢。于是 systemd 没有读取到我的配置,没有自动启动任何服务……于是只好在~/.profile里加两行,在挂载之后告诉 systemd 一声:

if [ -r "$HOME/.ecryptfs/auto-mount" ]; then
  grep -qs "$HOME ecryptfs" /proc/mounts
  if [ $? -ne 0 ]; then
    mv $HOME/.Xauthority /tmp 2>/dev/null
    mount -i "$HOME"
    cd "$HOME"
    mv /tmp/.Xauthority $HOME 2>/dev/null
    systemctl --user daemon-reload
    systemctl --user default
  fi
fi
Category: Linux | Tags: linux systemd
2
2
2014
1

玩转 systemd 之基于 socket 激活的服务

这几天闲下来的时间多了,于是趁机折腾 systemd,也读了不少 systemd 的文档。

Arch 官方宣布 sysvinit 不再被支持的时候,我是有些不喜欢的,因为我还没来得及弄明白 systemd 这完全不同的一套东西。现在看了不少 systemd 的文档,反倒是喜欢上 systemd 了 :-)

关于 systemd,我有两个没想到。其一,systemd 是兼容 sysvinit 服务的,一如 upstart。只是 Arch 一向比较激进,所以根本没用到这兼容性。其二,systemd 这个名字是个双关语,它不仅表示「system daemon」,还与「System V」遥相呼应,因为它是「System D」 :-)

本文不是教程,因此没什么意思的服务启动停止之类的就不写了。本文写点有意思的:让 systemd 监听套接字,在有连接时再启动服务。这不是什么新鲜东西,inetd 就是干这个的,但是我从来没用过,也没感觉有多大的需求。然而整理我的用户级服务时却发现这东西挺好的。

首先来最简单的,使用 sshd.socket 代替 sshd.service

[Unit]
Conflicts=sshd.service
Wants=sshdgenkeys.service

[Socket]
ListenStream=22
Accept=yes

[Install]
WantedBy=sockets.target

其实用起来很简单,systemctl start sshd.socket就启动它了。因为写了Conflicts=sshd.service,所以已经启动的 sshd 服务会自动停止。但是,我还没告诉 systemd 要监听 2 号端口而不是 22 呢!

直接改这个sshd.socket显然不行,下次更新修改就没了。把文件从/usr/lib/systemd/system复制到/etc/systemd/system下再修改?以前我是这么做的,但是其实还有更好的做法:

/etc/systemd/system下建立目录sshd.socket.d,然后建立个.conf文件写入需要的修改

[Socket]
ListenStream=
ListenStream=2

这里有两个ListenStream指示。第一个值为空,是重置该选项的值,之前的设置全部作废。systemd 单元文件中有很多选项都接受多个值,写多遍的话就是多项相加,除非写空值来重置前边设置过的值。一开始看到这种设计我还没明白为什么要这样做,后来看到对.d目录的支持才恍然大悟。

于是乎,自己要的修改完全和系统自带的配置分离开了,既不需要手动合并上游的新配置,也不需要担心自己复制过来修改的配置文件陈旧了。

除了sshd.socket文件外,还有一个与之配套的sshd@.service文件,说明服务该如何启动。当然像 acpid 这种Accept=false(默认)的套接字配置,有连接时只要启动一个进程来处理就可以了,所以对应的 .service 文件不是模板(文件名中没有@)。

下面我自己来给 socat 写个类似的配置。这个极其简单的服务是我为在远程主机中 fcitx.vim 来控制本地 fcitx用的。

首先是.socket文件:

[Socket]
ListenStream=@fcitx-remote
Accept=yes

[Install]
WantedBy=sockets.target

ListenStream第一个字符是@,表示「抽象套接字」,是 Linux 特有的一种在文件系统和网络之外的套接字,好处是不用在监听前先删除相应的套接字文件。socat 和 sshd 一样,也是一个连接对应一个进程,所以Accept=true,让 systemd 接受连接之后把连接的套接字传过来。

然后是对应的.service文件:

[Unit]
Description=Fcitx Socket Forwarder

[Service]
SyslogIdentifier=fcitx-socat
SyslogFacility=local0
ExecStart=/usr/bin/socat stdin tcp:10.7.0.6:8989
StandardInput=socket
StandardError=syslog

StandardInput=socket指定 socat 进程的标准输入是 systemd 接收的套接字,所以 socat 命令变成了这样子:

/usr/bin/socat stdin tcp:10.7.0.6:8989

这样子,原先跑在 tmux 里的命令

socat abstract-listen:fcitx-remote,fork tcp:10.7.0.6:8989

就变成由 systemd 监听,在需要时再启动 socat 来处理啦 =w=

关于这个 .service 文件中那两个 Syslog 开头的指令,以及这两个手写的配置要如何给 systemd 用,请看下篇:玩转 systemd 之用户级服务管理

Category: Linux | Tags: linux systemd
1
15
2014
12

被 Tornado coroutine 对异常的异常支持坑了

>>> python -m this | grep -A1 -F Errors
Errors should never pass silently.
Unless explicitly silenced.

因为要捕获子进程的标准输出、标准错误以及退出状态码,用 callback 写会非常麻烦,因为三者全部完成才能进行下一步操作。而使用 Tornado 的 coroutine 就很方便了,示例如下:

from tornado.gen import coroutine, Task
from tornado.process import Subprocess

@coroutine
def run_cmd(cmd):
    p = Subprocess(
        cmd,
        stdout = Subprocess.STREAM,
        stderr = Subprocess.STREAM,
    )
    out, err, code = yield [Task(p.stdout.read_until_close),
                            Task(p.stderr.read_until_close),
                            Task(p.set_exit_callback)]
    return out, err, code
    # For Python below 3.3, use
    # raise Return((out, err, code))

yield 一个 Task(或者 Future)的列表的话,它们会并发执行,全部执行完毕之后才会返回到这个 yield 位置继续执行。简洁干净。(不过我要吐槽一下为什么必须传列表,传元组就不对……)

于是乎,调用各种外部命令的部分被我由一堆回调改成了 coroutine,除了 yield 关键字有些别扭外,整个代码可读性好多了 :-)

可是后来,发生了这样的一件事:通过日志能看到一个 coroutine 前边的代码执行了,而后边的代码却没有执行,中间也没有 yield 到别的地方去!看上去非常诡异。

恰好前些天刚好看到一很不错的 Python 调试器 pudb。于是去执行中断的地方打断点(import pudb; pu.db),然后单步跟踪。这才发现原来是中间有个语句抛出了异常,然后这个异常被 coroutine「吃掉」了……示例代码如下:

#!/usr/bin/env python3

from tornado.gen import coroutine
from tornado.ioloop import IOLoop

@coroutine
def two():
  print('two entered')
  1 / 0
  print('two leaving')

@coroutine
def one():
  print('one entered')
  yield two()
  print('one leaving')

if __name__ == '__main__':
  one()
  IOLoop.current().start()

结果是:

one entered
two entered

执行从发生异常的那个位置中断了,并且没有任何错误消息被记录。(PS: 要是在 coroutine 里使用 try...except 的话是能抓到它的。)

以「tornado coroutine exception」为关键字找到了这个以及这个。原来 coroutine 的异常是被它返回的那个 Future 对象「吃掉」了。如果是在 Tornado 的 HTTP 服务里(RequestHandler),Tornado 的 web 模块会处理并记录这种异常。然而我是在 web 模块之外使用的,所以得自己来处理了:

#!/usr/bin/env python3

from tornado.gen import coroutine
from tornado.ioloop import IOLoop

@coroutine
def two():
  print('two entered')
  1 / 0
  print('two leaving')

@coroutine
def one():
  print('one entered')
  yield two()
  print('one leaving')

def _future_done(fu):
  fu.result()

if __name__ == '__main__':
  fu = one()
  fu.add_done_callback(_future_done)
  IOLoop.current().start()

这样就能看到有异常发生了:

one entered
two entered
ERROR:concurrent.futures:exception calling callback for <Future at 0x7f286c0bcf90 state=finished raised ZeroDivisionError>
  ...
  File "t.py", line 9, in two
    1 / 0
ZeroDivisionError: division by zero

那个异常的 Traceback 很长很长。没有原生的良好的协程支持的代价吧,不知道 Python 3.4 的 asyncio 里会不会好一些。

2014年8月2日更新:asyncio 在遇到这种情况时会打印错误日志,参见文档

Category: python | Tags: python tornado coroutine
1
11
2014
5

使用 inspect.lua 查找 Awesome 配置引入的内存泄漏

所谓「相见恨晚」,说的就是我第一次看到 inspect.lua 的感觉啊!Lua 这个超小型主打嵌入的语言,连 Readline 都要第三方库来支持,自然是没 Python 那样的补全功能了。不仅如此,连一个展示其数据结构的函数都没有。包括自己在内,不少人零零散散写过各种打印 Lua 表的函数,但像 inspect.lua 这样子优秀的还是第一次见到。

基本用法啊示例什么的不说了,直接在对象上调用 inspect 函数就可以得到一个(可能是巨大的但一定不会是无限的)字符串表示,递归的结构会依据其类型和一个序号来辨识。

既得此神器,自然要用来看看我那自从升级到 3.5 版本之后就一直在慢慢泄漏内存的 Awesome 了:

>>> awesome-client
awesome#inspect = dofile('tmpfs/inspect.lua')
awesome#f = io.open('tmpfs/output.lua', 'w')
awesome#f:write(inspect(_G))
awesome#f:close()
awesome#f = nil

然后就是慢慢察看了。我注意到了这么一个变量:

  raise_on_click = {
    [<client 13>] = true,
    [<client 12>] = true,
    [<client 14>] = true,
    [<client 15>] = true,
    [<client 16>] = true,
    [<client 17>] = true,
    [<client 18>] = true,
    [<client 19>] = true,
    [<client 20>] = true,
    [<client 21>] = true,
    ...
  }

这个变量由来已久,好像现在已经偏离了当初的设计目的了……不管它,反正呢,它里边保留了所有 Awesome 正管理的客户端对象,有 100 多个呢。可是,我怎么会同时有那么多窗口呢,明明才十几个啊?

检查一下配置文件,终于知道问题在哪里了:

diff --git a/rc.lua b/rc.lua
index ad06296..c1422bd 100644
--- a/rc.lua
+++ b/rc.lua
@@ -991,7 +991,7 @@ end)
 client.connect_signal("focus", function(c) c.border_color = beautiful.border_focus end)
 client.connect_signal("unfocus", function(c) c.border_color = beautiful.border_normal end)

-client.add_signal("unmanage", function(c)
+client.connect_signal("unmanage", function(c)
     raise_on_click[c] = nil
 end)
 -- }}}

add_signal是 Awesome 3.4 的用法,3.5 应该用connect_signal才对。这里的client.add_signal是 Awesome 自己用的另外一个意思的函数……

Category: 编程 | Tags: Lua awesome
12
29
2013
30

rsync+btrfs+dm-crypt 备份整个系统

生成目录!

目标:增量式备份整个系统

怎么做到增量呢?rsync + btrfs 快照。其实只用 rsync 也是可以做到增量式的1,但是支持子卷的 btrfs 可以做得更好:

  1. 快速删除旧的备份
  2. 更简单的备份逻辑
  3. 子卷可以设置成只读(这是个很重要的优点哦~)
  4. btrfs 支持压缩。系统里有好多文本文件的,遇到压缩效果不好的文件 btrfs 会自动放弃压缩

为什么要备份整个系统呢?——因为配置一个高度定制化的系统麻烦啊,只备份部分数据的话还可能漏掉需要的文件。另一个优点是可以直接启动到某个备份

备份了整个系统,包括各种公开或者隐私的数据,一堆 cookies 和帐号配置,邮件和聊天记录等。难道就不需要加密一下下吗?于是,在 btrfs 之下再加一层 dm-crypt 加密。

介绍完毕,下边进入正题。

准备工作

首先,需要一个足够新的 Linux 内核,因为 btrfs 还是「实验特性」,每个版本都会有大量改进。如果用比较旧的内核就有可能出事。我用的是 3.12.6 版本。其次,安装 rsync 和 cryptsetup。

当然还要准备硬件:一块希捷 BackupPlus 1T USB 3.0 移动硬盘,以及一枚Express Card 34mm 转 USB 3.0 扩展卡,因为我的笔记本没有 3.0 的接口。注意使用 USB 3.0 扩展卡,内核需要载入 pciehp 模块,否则会出现不能识别 3.0 的设备或者后续接上去的设备的情况。Arch 官方内核将这个模块直接编译进内核了,而我自己编译的很不幸没有,只好重新编译了下内核。

PS: 这俩家伙一起配合工作,写入峰值能达到 110MiB/s,比我笔记本自身的硬盘还要快。

然后是分区、格式化。我使用了 GPT 分区表。为了安装 grub 以便启动,最好在开头分配 2M 空间给 grub 使用,不然会很麻烦2。记得给这个分区 bios_grub 标志(GParted「管理标志」里勾上即可)。下一个分区是 ext4 格式的启动分区。我会在这里放一个 Arch Linux Live 系统用于维护任务,以及用于启动到备份的内核和 initramfs。因为备份的分区会被加密,所以必须把内核和 initramfs 放在另外的地方。接下来的一个分区放加密过的备份数据用的。

像这样初始化加密分区:

cryptsetup luksFormat /dev/sdc3

密码要长,但也一定要记住密码,因为除了穷举外是没有办法恢复的。

初始化完毕之后就可以使用密码打开该设备了:

cryptsetup open /dev/sdc3 lilybackup

最后的参数是一个名字,它会是解密后的设备在 /dev/mapper 下的文件名。

如果一切完毕,要记得(在卸载文件系统之后)关闭该设备:

cryptsetup luksClose lilybackup

dm-crypt 是块设备级的加密。我们还要在其上建立文件系统:

mkfs.btrfs /dev/mapper/lilybackup

然后挂载之,并建立相应的目录和子卷结构。比如我的:

* backup (dir)
  * home (dir)
    * current (subvol, rw)
    * 20131016_1423 (subvol, ro)
    * 20131116_2012 (subvol, ro)
    * ...
  * root (dir)
    * current (subvol, rw)
    * 20131016_1821 (subvol, ro)
    * 20131116_2128 (subvol, ro)
    * ...
* run, for boot up directly, with edited /etc/fstab (dir)
  * home (dir)
    * 20131116 (subvol, rw)
    * ...
  * root (dir)
    * 20131116 (subvol, rw)
    * ...
* etc, store information and scripts (subvol, rw)

我把备份数据放到 backup 目录下,/ 和主目录 /home/lilydjwg 分开备份的。每个备份是使用日期和时间命名的快照子卷。除了用于每次同步的 current 目录外其它的子卷都是只读的,以免被意外修改。在 run 目录下是用于直接运行的,可写。这些可以按需建立。

开始备份

这是我备份 / 使用的脚本。是一个 zsh 脚本,这样可以避免 bash 中特殊字符可能带来的问题,虽然 bash 有 shellcheck 可以静态分析出可能有问题的地方。

这个脚本带两到三个参数。第一个是 / 的位置,因为我一般会直接从运行的系统执行备份,但也有可能使用另外的维护系统(比如系统滚挂掉的时候)。第三个参数是用于确认操作的。不加它的话会以 --dry-run 参数来运行 rsync。rsync 很复杂,所以最好先演习一遍以避免不小心手抖了做错事 =w=

为了避免日后对照着 rsync 手册来揣摸每个单字母选项的意义,我在这里全部使用了选项的完整形式。反正我是 zsh 用户,那么长的命令中大部分字符都是 zsh 给我补全出来的 =w=

#!/bin/zsh -e

cd $(dirname $0)

if [[ $# -lt 2 || $# -gt 3 ]]; then
  echo "usage: $0 SRC_DIR DEST_DIR [-w]"
  exit 1
fi

src=$1
dest=$2
doit=$3

if [[ $doit == -w ]]; then
  dry=
else
  dry='-n'
fi

rsync --archive --one-file-system --inplace --hard-links \
  --human-readable --numeric-ids --delete --delete-excluded \
  --acls --xattrs --sparse \
  --itemize-changes --verbose --progress \
  --exclude='*~' --exclude=__pycache__ \
  --exclude-from=root.exclude \
  $src $dest $dry

比较重要的几个 rsync 选项:

--archive
我们要备份,所以请保留所有信息
--one-file-system
只备份这个文件系统的内容,不要跑到 /sys 啊 /proc 啊 /dev 啊 /tmp 这类目录里去了。这也省得自己手动排除
--numeric-ids
文件的所有者信息使用数字而不要解析成用户名/组名。避免在跨系统使用时出差错
--exclude-from=root.exclude
root.exclude文件中读取额外的排除列表
--acls --xattrs
保留文件 ACL 和扩展属性

我发现的 / 里需要排除的目录如下:

/var/cache/*/*
/var/tmp/
/var/abs/local/
/var/lib/mongodb/journal/

其中第一项写成那样是因为,我要保留 /var/cache 下的一级目录。

主目录的备份是类似的过程,只是更加复杂罢了。当我写我的主目录的备份脚本的时候,深切地体会到有圣人说过的一句话——过早的优化是万恶之源。因为我使用 eCryptfs 加密主目录时为了避免可以公开的文件被加密造成性能损失,做了一系列的软链接。它们一直在给我带来各种小麻烦和不爽……

注意这些脚本要以 root 的身份运行。待所有备份脚本跑完之后,对那个 current 子卷做一个只读快照就好了:

sudo btrfs subvolume snapshot -r current $(date +'%Y%m%d_%H%M')

下次要更新备份时是一样的步骤:跑同步脚本,创建新快照。第一次同步会比较慢,跑了一个多小时吧。后边的增量备份就比较快了,十几分钟就好。

从备份启动

要启动,首先把 grub 装过去。把 Arch Linux live 系统的配置写好,当然还有启动备份系统的配置,如下:

search --no-floppy --fs-uuid --set=root 090dcc64-2b6d-421c-8ef6-2ab3321aec62

menuentry "Archlinux-2013.12.01-dual.iso (x86_64)" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    set isofile="/images/archlinux-2013.12.01-dual.iso"
    echo "Setup loop device..."
    loopback loop $isofile
    echo "Loading kernel..."
    linux (loop)/arch/boot/x86_64/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
    echo "Loading initrd..."
    initrd (loop)/arch/boot/x86_64/archiso.img
}

menuentry "Archlinux-2013.12.01-dual.iso (i686)" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    set isofile="/images/archlinux-2013.12.01-dual.iso"
    echo "Setup loop device..."
    loopback loop $isofile
    echo "Loading kernel..."
    linux (loop)/arch/boot/i686/vmlinuz archisolabel=ARCH_201312 img_dev=/dev/disk/by-label/lilyboot img_loop=$isofile earlymodules=loop
    echo "Loading initrd..."
    initrd (loop)/arch/boot/i686/archiso.img
}

set ver=3.12.6
menuentry "Arch Linux $ver backup" {
    load_video
    set gfxpayload=keep
    insmod gzio
    insmod ext2

    echo    'Loading Linux kernel ...'
    linux   /boot/vmlinuz-linux-lily-$ver root=/dev/mapper/lilybackup rw cryptdevice=/dev/disk/by-uuid/815d01ea-6390-460f-8c82-84c9e9497423:lilybackup rootflags=compress=lzo,subvolid=0 break=postmount
    echo    'Loading initramfs...'
    initrd  /boot/initramfs-$ver-backup.img
}

各个设备的卷标、UUID 和文件路径自己调整。最后一项需要的文件在后边准备,先介绍一下几个参数:

root
根分区所在的设备。是解密后的设备路径或者用 UUID 也可以。不过没关系的,这里不会有冲突的
cryptdevice
如果使用密码(而不是密钥文件)加密的话,这里是冒号分隔的两个参数:你的加密设备是哪个文件,以及它解密之后叫什么名字。
rootflags
这个是给我们的 btrfs 用的。指定要启用 lzo 算法压缩,使用根子卷。实际上根子卷里不是 Linux 系统的根。这里我只是让脚本把它挂载到/new_root上而已,脚本会因为找不到/sbin/init而进入一个 shell 的
break=postmount
即使根没有问题,也请在挂载好它之后给我一个 shell,我可能需要做一些调整

内核很简单,直接 cp 过去就好了。initramfs 要另外生成。这是我用来生成的 mkinitcpio.conf.crypt 文件:

MODULES="btrfs"
BINARIES="/usr/bin/btrfs"
FILES=""
HOOKS="base udev autodetect modconf block encrypt filesystems keyboard fsck shutdown"
COMPRESSION="xz"

重要的地方:内核模块 btrfs 一定是要的,另外我还需要 btrfs 程序来操作子卷。在 HOOKS 数组的 filesystems 前添加了 encrypt,用于在启动时询问密码并解码根分区。

我还准备添加 vi 程序来着,但是它说终端类型不认识,还说 /var/tmp 目录不存在。于是索性把自己静态链接的 vim 扔到内核一块去了。Vim 内建常见终端类型的数据,不那么挑剔的。zsh 所需要的文件太多,也放弃了。

然后使用 mkinitcpio 命令生成 initramfs 镜像:

sudo mkinitcpio -c mkinitcpio.conf.crypt -g initramfs-backup.img

然后把生成的文件复制到启动分区的相应路径下。

注意:如果你在 Arch Linux live 系统中为另外的内核生成该 initramfs,要指定内核和内核模块路径的根。一定不要将内核模块所在的目录软链接到/lib/modules,那样 mkinitcpio 不会添加任何块设备的内核模块的。我使用的命令如下:

mkinitcpio --kernel /run/archiso/img_dev/boot/vmlinuz-linux-lily-3.12.6 -r /mnt/backup/root/20131227_2044 --config mkinitcpio.conf -g /run/archiso/img_dev/boot/initramfs-3.12.6-backup.img

文件准备完毕,就可以启动过去了。注意我没有在备份分区的 run 目录下建立子卷,因为我准备进入 initramfs 之后再建立它们。

PS: 因为 BIOS 不支持,所以必须从 USB 2.0 来启动。在 Linux 内核启动的时候,可以将移动硬盘接到 USB 3.0 扩展卡上。具体时机是,initramfs 载入完毕,内核开始打印日志的时候。

进入备份系统

启动之后,会进入 initramfs 的 shell。在这个没有任务管理的 ash 中,使用 btrfs 命令在 /new_root/run 目录下建立新的子卷,注意不要加 -r 这个表示只读的选项了:

btrfs subvolume snapshot ../backup/root/20131016_1423 root/20131016
btrfs subvolume snapshot ../backup/home/20131016_1423 home/20131016

因为文件系统树的挂载结构变了,所以得拿准备好的 vim(或者 vi,如果你没准备 vim 的话)去编辑 root/20131016/etc/fstab 文件,将那些不会成功的挂载项都去掉,添加新的正确的项。PS: 如果使用 vim 的话,记得进去先set nocp一下,不然会是兼容模式,和 vi 一样只能撒消一步的。

/dev/mapper/lilybackup  /       btrfs   rw,relatime,compress=lzo,subvol=run/root/xxx     0 0
/dev/mapper/lilybackup  /home/lilydjwg  btrfs   rw,relatime,compress=lzo,subvol=run/home/xxx     0 0

然后 cd /,卸载 /new_root 并重新以子卷挂载之:

cd /
umount /new_root
mount -o compress=lzo,subvol=run/root/20131016 /dev/mapper/lilybackup /new_root

如果是因为找不到 /sbin/init 而进来这个 shell 的,那么就tail /init,最后那行是需要执行的命令(当然有些修改):

exec env -i "TERM=$TERM" /usr/bin/switch_root /new_root /sbin/init

如果是因为break=postmount参数而进来的,直接按Ctrl-D退出 shell 即可。

启动会继续进行,systemd 启动了,各种服务陆续启动中~~

如果很不幸地,忘记修改/etc/fstab了,或者有错,那么 systemd 会毫不留情地「Welcome to emergency shell」。不过现在更正也为时未晚。在编辑完 fstab 之后,要先执行下systemctl daemon-reload再退出那个 emergency shell。

接下来应该能一路顺利地到达指定时间的系统啦 =w=

时间机器打造完成哦耶~~

参考资料

Category: Linux | Tags: linux grub grub2 btrfs Arch Linux
12
14
2013
12

豌豆荚:黑水洗不白应用

豌豆荚说火狐更新了,但是签名变了,需要谨慎升级:

豌豆荚:火狐有更新

于是,本来应试是这样子的火狐:

火狐国际版:热门网站 火狐国际版:书签

变成了这个样子,桌面还多了个奇怪的图标:

火狐中国版:热门网站 火狐中国版:书签 火狐中国版:「二维码」图标

我看到这堆乱七八糟的网站之后,只好自行去 Mozilla 的 FTP 站点下载国际版火狐,并验证了一下签名:

29674 ~tmp/firefox
>>> wget ftp://ftp.mozilla.org/pub/mozilla.org/mobile/releases/latest/android/multi/fennec-26.0.multi.android-arm.apk ftp://ftp.mozilla.org/pub/mozilla.org/mobile/releases/latest/android/multi/fennec-26.0.multi.android-arm.checksums ftp://ftp.mozilla.org/pub/mozilla.org/mobile/releases/latest/android/multi/fennec-26.0.multi.android-arm.checksums.asc
29675 ~tmp/firefox
>>> gpg --recv-keys 15A0A4BC
gpg: 下载密钥‘15A0A4BC’,从 hkp 服务器 keys.gnupg.net
gpg: 密钥 3A06537A:公钥“Mozilla Software Releases <releases@mozilla.org>”已导入
gpg: 需要 3 份勉强信任和 1 份完全信任,PGP 信任模型
gpg: 深度:0 有效性:  1 已签名:  4 信任度:0-,0q,0n,0m,0f,1u
gpg: 深度:1 有效性:  4 已签名:  0 信任度:4-,0q,0n,0m,0f,0u
gpg: 合计被处理的数量:1
gpg:           已导入:1  (RSA: 1)
29676 ~tmp/firefox
>>> gpg --verify fennec-26.0.multi.android-arm.checksums.asc
gpg: 于 2013年12月06日 星期五 01时11分05秒 CST 创建的签名,使用 RSA,钥匙号 15A0A4BC
gpg: 完好的签名,来自于“Mozilla Software Releases <releases@mozilla.org>”
gpg: 警告:这把密钥未经受信任的签名认证!
gpg:       没有证据表明这个签名属于它所声称的持有者。
主钥指纹: 2B90 598A 745E 992F 315E  22C5 8AB1 3296 3A06 537A
子钥指纹: 5445 390E F5D0 C2EC FB8A  6201 057C C3EB 15A0 A4BC
29677 ~tmp/firefox
>>> grep -F fennec-26.0.multi.android-arm.apk fennec-26.0.multi.android-arm.checksums
f6b14fbb5847cd87c8821cfd71beba8f64c7059e05a555c31e6ea8168905a54def6be8d20b0766b5f35f10b63efb73225d20cb4bfa04383b66d396d33f743242 sha512 26553667 fennec-26.0.multi.android-arm.apk
b20f6faa9d88171fb504eaf1a323918c md5 26553667 fennec-26.0.multi.android-arm.apk
5ffb036fa664fbbb59e750ec50b44a781af9565e sha1 26553667 fennec-26.0.multi.android-arm.apk
29678 ~tmp/firefox
>>> sha1sum fennec-26.0.multi.android-arm.apk
5ffb036fa664fbbb59e750ec50b44a781af9565e  fennec-26.0.multi.android-arm.apk

顺便看了看这两个版本火狐签名用的密钥:

29753 ~tmp/firefox
>>> unzip -p fennec-26.0.multi.android-arm.apk META-INF/RELEASE.RSA | keytool -printcert
所有者: CN=Release Engineering, OU=Release Engineering, O=Mozilla Corporation, L=Mountain View, ST=California, C=US
发布者: CN=Release Engineering, OU=Release Engineering, O=Mozilla Corporation, L=Mountain View, ST=California, C=US
序列号: 4c72fd88
有效期开始日期: Tue Aug 24 07:00:24 CST 2010, 截止日期: Sat Jan 09 07:00:24 CST 2038
证书指纹:
         MD5: B1:E1:BC:EE:27:33:02:5E:CE:94:56:E4:19:A8:14:A3
         SHA1: 92:0F:48:76:A6:A5:7B:4A:6A:2F:4C:CA:F6:5F:7D:29:CE:26:FF:2C
         SHA256: A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04
         签名算法名称: SHA1withRSA
         版本: 3
29754 ~tmp/firefox
>>> unzip -p org.mozilla.firefox-1.apk META-INF/RELEASE.RSA | keytool -printcert
所有者: CN=Release Engineering, OU=R&D, O=Mozilla Online, L=Beijing, ST=Beijing, C=CN
发布者: CN=Release Engineering, OU=R&D, O=Mozilla Online, L=Beijing, ST=Beijing, C=CN
序列号: 514de131
有效期开始日期: Sun Mar 24 01:06:57 CST 2013, 截止日期: Thu Aug 09 01:06:57 CST 2040
证书指纹:
         MD5: D7:C9:FC:AE:D0:3B:9F:24:43:33:DD:41:15:65:CD:8A
         SHA1: 1A:97:FA:6D:9D:83:DB:84:BE:77:1C:72:60:42:48:7C:85:83:D4:60
         SHA256: 02:A7:BC:81:02:74:C9:ED:38:93:3B:06:3D:1A:48:A8:4F:88:CB:11:C6:40:CA:A1:6C:F8:AA:FA:66:E3:C6:3A
         签名算法名称: SHA1withRSA
         版本: 3

那个中国特色的版本的密钥声称自己是 Mozilla 北京。估计和电脑上的中国版火狐一样,真是他们搞的吧。其实 Opera Mobile 我也不小心装过中国版的(名叫「欧朋浏览器」),不过比火狐中国版稍微好点,首页上那堆我用不到的网站很容易就删掉了,而中国版火狐我没能找到要怎么办。

看来以后一些手机应用的升级得自己来做了呢,标榜能「洗白白」应用的豌豆荚不小心就会给我塞点一堆广告的中国版应用……

Category: Android | Tags: Android 火狐 中国特色

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com