11
10
2016
13

数据让 git 给吃了!

之前一直觉得 git 是很安全的,除非用户显式指定(比如 --force 啦,reset --hard 啦,checkout xxxx 啦),git 在用户会失去数据时都会停下来,让不小心的用户有机会处理被遗忘的修改。直到有一天,我们有个文件让 git 给吃了!

嗯,是「我们」,不是「我」。这是我们的代码部署服务器上出的事。这仓库不是我使用的,整个操作流程我也没有参与设计与评估。实际上我只是作为 troubleshooter 参与到这次神秘事件之中的。

要让 git 愉快地吃掉数据,只要这样就可以了:

  • 提交 A 不包含文件 f
  • 提交 B 包含文件 f
  • 当前工作区为提交 A,并且包含一份未被 git 管理的文件 f,并且 f 被 gitignore 忽略掉了

然后做如下操作,未被管理的那份 f 就会消失不见了:

  • 将工作区切换到提交 B。因为 f 被忽略,所以 git 不会报错(代码
  • 将工作区再切换回 A。因为 A 不包含 f,所以 f 被删掉了

正在吃 f 的 git:主人遗弃了的 f 就交给我好了~

要避免出现这种问题,当然是在 git 工作区会有修改的时候,不要依靠 git 来在多个版本间切换啦~btrfs 或者 zfs 的快照多好!如果文件系统不支持快照的话,那就用多个目录吧。

Category: 版本控制 | Tags: linux Git
11
3
2016
15

诡异多多的 bash

要说哪个 shell 最复杂难学,我肯定回答 zsh。而要说哪个 shell bug 最多,毫无疑问是 bash 了。shellshock 这种大家都知道的我就不说了。bash 有很多很诡异的角落,昨天我亲身碰到一个。

我有一个 Python 程序 A,会使用 subprocess 带 shell=True 跑一行 shell 命令。那条命令会在后台跑另外一个 Python 程序 B。诡异的事情是,当我向 B 的进程发送 SIGINT 时,无法结束它,以及它下边带的一个 tail 进程。一开始我还没注意到 B 的进程本身没有被 SIGINT 杀死,是在无效的情况下被 A 用 SIGKILL 杀死的。我只看到那个 tail 程序还活着。所以我去处理了一下 KeyboardInterrupted 异常,来结束掉那个 tail。

结果很诡异:KeyboardInterrupted 异常并没有发生。通过 strace 观察可以看到,B 进程在读 tail 的输出,然后收到了 SIGINT,然后接着读 tail 的输出……我一开始还以为这个和 PEP 475 相关,以为是 Python 自动重启了被中断的系统调用,所以没来得及处理信号(Python 的信号并不是及时处理的)。然后就去仔细看文档。结果文档告诉我,如果注册了信号处理函数,并且它抛出异常的话,那么被中断的系统调用是不会被重试的。所以这就不对了。

然后我又测试了直接在终端运行 B,而不是通过 A 去运行。本来我开发的时候就是这么测试它的,也没遇到什么怪异的现象。结果确实没有什么怪异的事情发生:即使我使用 kill 命令只给 B 发送 SIGINT 信号,Python 的 KeyboardInterrupted 逻辑会被触发,然后它主动杀掉 tail 进程。(使用 Ctrl-C 的话,B 和 tail 都会收到 SIGINT 信号的。)

疑惑的时候,我又想到了拿 SIGINT 去杀那个不死的 tail 进程,这才发现它也出现奇怪的行为了:正在读 inotify 的文件描述符呢,来了个 SIGINT 信号,然后它接着读 inotify 去了……跟 B 出现的问题一样。我又去查了 tail.c 的源码,也没发现它对 SIGINT 有特殊的处理啊。

难道是继承过来的?man 7 signal 了一下,果然:

During an execve(2), the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged.

所以 tail 和 B 继承了一个「忽略 SIGINT」的行为。(nohup 就是用的类似的手段啊。)

于是 strace -f 了整个从 A 开始的进程树,最后发现这问题和 Python 并没有什么关系,而是 bash 的错!

A 是用 shell=True 调用的命令,所以它调用了 /bin/sh。系统是 CentOS,所以 /bin/sh 是指向 bash 的。所以这里实际上调用了 bash,而它的处理有问题。

要重现这个 bug 很容易:

bash -c 'sleep 1000 &'

然后这个 sleep 进程就会忽略 SIGINT 和 SIGQUIT 了。我也不明白 bash 这是想要做什么。

之前也遇到过另外几个 bash 的 bug(或者是 feature?)——

  1. 在终端中,在脚本中执行交互式 bash 时,第一个 bash 进程会将自己设为前台进程组,导致后来的进程收到 SIGTTIN 或者 SIGTTOU。很神奇,两行同样的命令,第一条和后边的行为不一致

  2. 在 bash 中,执行不带 shebang 的 shell 脚本时,脚本会在当前 bash 进程内执行,造成 history 命令的行为异常

  3. 这个是听说的。输出失败时,未写入目标的内容仍留在缓冲区内,会在奇怪的地方冒出来

以后还是尽量避开 bash 吧。有 zsh 用 zsh,有 dash 用 dash;它们都没有本文提到的这些问题。

Category: Linux | Tags: shell bash
10
21
2016
0

在 Python 里设置 stdout 的编码

有时候进程的运行环境里,locale 会被设置成只支持 ASCII 字符集的(比如 LANG=C)。这时候 Python 就会把标准输出和标准错误的编码给设置成 ascii,造成输出中文时报错。

一种解决办法是设置支持 UTF-8 的 locale,但是那需要在 Python 进程启动前设置。启动之后,初始化过了,再设置 locale 也不会重新初始化那些对象。

另一种办法是往 sys.stdout.buffer 这种地方直接写 bytes。理论上完全没问题,但是写起程序来好累……

我就去找了一下怎么优雅地弄一个新的 sys.stdout 出来。Python 3 的 I/O 不再使用 C 标准库的 I/O 函数,而是直接使用 OS 提供的接口。封装位于 io 这个模块里边,有带缓冲的,不带缓冲的,二进制的,文本的。

研究了一下文档可知,sys.stdout 是个 io.TextIOWrapper,有个 buffer 属性,里边是个 io.BufferedWriter。我们用它造一个新的 io.TextIOWrapper,指定编码为 UTF-8:

import sys
import io

def setup_io():
  sys.stdout = sys.__stdout__ = io.TextIOWrapper(
    sys.stdout.detach(), encoding='utf-8', line_buffering=True)
  sys.stderr = sys.__stderr__ = io.TextIOWrapper(
    sys.stderr.detach(), encoding='utf-8', line_buffering=True)

这里除了可以设置编码之外,也可以设置错误处理和缓冲。所以这个技巧也可以用来容忍编码错误、改变标准输出的缓冲(不需要在启动的时候加 -u 了)。

其实这样子还是不够彻底。Python 在很多地方都有用到默认编码。比如 subprocess,指定 universal_newlines=True 时 Python 会自动给标准输入、输出、错误编解码,但是呢,在 Python 3.6 之前,这里的编码是不能手动指定的。还有参数的编码,也是不能指定的(不过可以传 bytes 过去)。

所以,还是想办法去设置合适的 locale 更靠谱……

Category: python | Tags: Python 中文支持 linux
9
13
2016
7

Linux 下的 Wi-Fi 分享

首先看看你的网卡和驱动组合是否支持这样的操作。

>>> iw list | grep -A2 combinations:
        valid interface combinations:
                 * #{ managed } <= 1, #{ AP, P2P-client, P2P-GO } <= 1, #{ P2P-device } <= 1,
                   total <= 3, #channels <= 2

上边这个输出说明支持,并且频道可以不一样。

然后,添加一个用途 AP 的网络接口,并配置 IP 地址。我的无线网络接口名字是 wlan0,因为我通过创建空 /etc/udev/rules.d/80-net-setup-link.rules 文件的方式禁用了 systemd 的网络接口改名。

sudo iw dev wlan0 interface add wlan0_ap type __ap
sudo ifconfig wlan0_ap 192.168.17.1

配置 NAT:

echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
sudo iptables -w -t nat -A POSTROUTING -s 192.168.17.0/24 -j MASQUERADE

配置 DHCP。我用的是 dnsmasq。它本来是作为 DNS 缓存用的,但是也支持 DHCP,那就用它了:

interface=wlan0_ap
no-dhcp-interface=wlan0
dhcp-range=192.168.17.50,192.168.17.150,12h

注意不要在其它只提供 DNS 服务的接口上提供 DHCP 服务,以免出现冲突。

然后就可以开启热点啦。hostapd 配置如下:

interface=wlan0_ap
driver=nl80211
ssid=名字
channel=1
hw_mode=g
ieee80211d=1
country_code=cn
ieee80211n=1
ieee80211h=1
ignore_broadcast_ssid=0
auth_algs=1
wpa=2
wpa_passphrase=secret
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP

最后把它们跑起来就可以了。

为了方便使用,我创建了个 systemd 服务 wlan0_ap.service:

[Unit]
Description=Setup wlan0_ap
Before=hostapd.service
After=sys-subsystem-net-devices-wlan0.device
After=iptables.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/iw dev wlan0 interface add wlan0_ap type __ap
ExecStart=/usr/bin/ip address add dev wlan0_ap 192.168.17.1/24
ExecStart=/usr/bin/iptables -w -t nat -A POSTROUTING -s 192.168.17.0/24 -j MASQUERADE
ExecStop=-/usr/bin/iptables -w -t nat -D POSTROUTING -s 192.168.17.0/24 -j MASQUERADE
ExecStop=/usr/bin/ip address delete dev wlan0_ap 192.168.17.1/24
ExecStop=/usr/bin/iw dev wlan0_ap del

[Install]
WantedBy=hostapd.service

systemctl enable wlan0_ap 之后就可以直接 systemctl start hostapd 来启动了~当然也很容易停止服务:systemctl stop hostapd wlan0_ap。我的 dnsmasq 总是开启的,所以就不用加依赖了。还有 ipv4_forward 我也是早就写到配置文件 /etc/sysctl.d/99-sysctl.conf 里的。

Category: Linux | Tags: linux 网络 systemd
9
10
2016
0

Jupyter + matplotlib = ♥

matplotlib 是很不错的数据可视化库,然而每次写一个脚本,跑出来看完又回头改,改完再跑,实在是累。所以就有 IPython Notebook 啦,后来改名叫 Jupyter 了,不光支持 Python,还支持 Julia 什么的样子(在下一盘很大的棋呢)。

Arch Linux 用户使用以下命令安装:

sudo pacman -S jupyter-nbconvert jupyter-notebook

我没有装 mathjax 这个包。我就用 MathJax 官方的 CDN 地址好了。所以我的启动命令是这样子:

jupyter notebook --NotebookApp.mathjax_url=https://cdn.mathjax.org/mathjax/latest/MathJax.js

然后界面就会在浏览器里打开啦~

Jupyter notebook 最令我不爽的一点是,它的编辑区用起来很不习惯: 不支持 readline 式快捷键(就是 Emacs / bash 风格那些啦),不支持选中复制、中键粘贴 * 不支持补全

我尝试过配置快捷键,但是还是不太会的样子,好像又没有现成而且可用的代码。

不过它的可视化和交互能力实在是太吸引人了~所以做一些交互式的数据处理时还是用用好了~

这里是演示。(当然只是导出的 HTML 页面~)

Category: python | Tags: python 数据分析
9
10
2016
6

如何取消对 WoSign 根证书的信任

WoSign 最近曝出一大堆问题,而且其处理问题的态度、解决问题的方式十分令人担忧。其官方形象也很糟糕,比如对国内 Let's Encrypt 用户进行 FUD 式威胁,比如只吊销了因 bug 误发的 GitHub 域名的证书,给某大学误发的证书视若无睹。具体问题有兴趣的可以去相关邮件组查看讨论。

这次问题我认为比起 CNNIC 要严重多了(最主要是这态度、这水准,就算它不主动作恶,也很容易被利用的样子),所以我获知情况之后就取消对了 WoSign 的信任。StartCom 签名 WoSign 的证书,所以需要一并吊销(反正也是一家人)。

火狐(桌面版)

依次打开「首选项」->「高级」->「证书」->「查看证书」,找到并选择 StartCom 和 WoSign 下的所有证书(使用 Shift 键可以选择连续的项目),然后点「编辑信任」按钮,取消弹出框中三个选项框的勾选。

Arch Linux

archlinuxcn 源用户直接执行命令:

sudo pacman -Sy revoke-disputable-ca

手动操作的话,是这样子。把需要取消信任的证书复制(不要软链接)到 /etc/ca-certificates/trust-source/blacklist/ 目录下,然后执行 update-ca-trust 命令即可。

我那个包里取消信任的证书是下边这八个:

CA_WoSign_ECC_Root.pem                    CNNIC_ROOT.pem                          StartCom_Certification_Authority_G2.pem  WoSign_China.pem
Certification_Authority_of_WoSign_G2.pem  StartCom_Certification_Authority.1.pem  StartCom_Certification_Authority.pem     WoSign.pem

Android

在「设置」->「安全」->「受信任的凭据」中禁用掉相关证书。这对 Opera Mobile 有效,但是对火狐无效。

目前还没找到火狐 Android 版禁用根证书的方式。

确认方法

访问 https://www.wosign.com/ 即可。

目前 USTC 已经更改证书,禁用这些根证书不影响 USTC 镜像源的使用。现在我因此不能访问的网站主要是 Python 邮件列表

Category: 网络 | Tags: http ssl 安全
8
17
2016
14

换域名了

如题。域名换成了 blog.lilydjwg.me,别的暂时不变。浏览器访问时会自动跳转到新域名。

请 RSS 订阅读者更新订阅地址。

请网站上有链接到本博客的读者也更新一下旧链接。

我不知道旧域名到底能存在多久。当然也不知道这个网站还会存在多久。总之先把入口拿回来。

Category: 未分类 | Tags:
7
23
2016
0

发包太快,请勿跟踪

之前写的那个处理 DNS AAAA 的程序,后来请求量大的时候就经常报错。经过研究,是在sendto的时候返回了「Pemission Denied」错误。后来的 Rust 版本也发生了类似的问题,得到操作系统返回的代码「EPERM」。

我翻了半天 man 手册,其中只说到向广播地址发包可能会得到 EACCES 错误。Google 也没有得到结果(都是些权限不够的问题,但我的程序是 root 跑的呀,并且错误比较零星)。后来发到 shlug 邮件列表中询问,才终于得知了和我有同样问题的人,但是也没有结论,只是说关掉 iptables 就正常了。可我的程序依赖 iptables 呢……而且我要的不仅仅是解决方案(实际上这个问题并没有造成什么可感知的影响,就算有,我也有办法 migrate),我更想知道为什么。

确定是发包太快造成的问题,拿着相关关键词去搜,还真找到了一些有用的信息。比如之前看过的 CloudFlare 低延迟 UDP 实验时会让 iptables 不跟踪相关数据包,有人在使用 SIP 协议时也遇到了同样的问题,并且在内核日志的帮助下解决了。于是我照着做,让 conntrack 放过我发出的 UDP 包:

iptables -t raw -I OUTPUT -p udp -m udp --sport 53 -j NOTRACK

然后不仅那些错误都没了,而且处理速度快了一倍!(图中红虚线是发生错误的时候。)

程序统计信息

Category: 网络 | Tags: iptables linux UDP
6
17
2016
7

Linux 作业控制实践

事情的起因是这样子的。

有一个非常常用的调试工具叫 strace。输出的信息是纯文本,一大片看起来累。在 Vim 里可以给它高亮一下,就好看多了。再加上各种搜索、清理,以及非常赞的 mark.vim 插件,用起来就舒服多了!

然而我并不想每次都让 strace 写到文件里然后再拿 Vim 去读,因为还得记着清理那些文件。如果数据量不大的话,直接通过管道传给 Vim 多好。

于是有了如下 zsh 函数:

(( $+commands[strace] )) && strace () { (command strace "$@" 3>&1 1>&2 2>&3) | vim -R - }

效果是达到了,但是这样子要中断 strace 的话,得去另一个终端里去 kill。按 Ctrl-C 的话,SIGINT 也会被发给 Vim,导致 Vim 显示空白。

所以嘛,得把 Vim 放到一个单独的进程组里,这样就不会在 Ctrl-C 的时候收到 SIGINT 了。但是,Vim 还得用终端啊。

一开始,我用自制的 expect.py 模块,给 Vim 分配了一个新的终端。这样子 Ctrl-C 好用了。然后我发现 Ctrl-Z 不好用了……

Ctrl-Z 还是挺方便的功能,临时需要执行个命令,不用开新的 shell(以及 ssh),直接按一下 Ctrl-Z,完事之后再回来,多好啊!就跟 zsh 的 Alt-q 一样方便好用呢。

于是就想还是不开 pty 了。直接子进程放新组里跑。这样 Vim 在尝试向终端输出时会收到 SIGTTOU 信号,因为它不是前台进程组。找了一下,用 tcsetpgrp 就可以把指定进程组放到前台了。然后发个 SIGCONT 让可能已经停下来了的 Vim 继续。

然后,当 Vim 收到 SIGTSTP 而停止的时候,我的程序该怎么知道呢?搜了一下,原来这种情况下也会收到 SIGCHLD 的!我以前一直以为只有子进程退出才会收到 SIGCHLD 啊……然后是一个关于 SIGCHLD 的坑,之前在 pssh 里看到过的,这次没有及时想到:不给 SIGCHLD 注册信号处理器时是收不到 SIGCHLD 的!不过诡异的是,我的这个程序有时却能够收到——在我使用 strace 跟踪它的时候……

于是,当 Vim 收到 SIGTSTP 时,把我们自己设置成前台进程组,然后给自己发一个 SIGTSTP 也停下来好了。令人意外的是,后台进程在调用 tcsetpgrp 时竟然也会收到 SIGTTOU。不过没关系,忽略掉就好了。

当用户 fg 时,就再把 Vim 设置成前台进程,并给它一个 SIGCONT 让它继续就好了。

最终的成品 vimtrace 在这里我的 zsh 配置是这样子的:

if (( $+commands[vimtrace] )); then
  (( $+commands[strace] )) && alias strace='vimtrace strace'
  (( $+commands[ltrace] )) && alias ltrace='vimtrace ltrace'
else
  (( $+commands[strace] )) && strace () { (command strace "$@" 3>&1 1>&2 2>&3) | vim -R - }
  (( $+commands[ltrace] )) && ltrace () { (command ltrace "$@" 3>&1 1>&2 2>&3) | vim -R - }
fi

后记:

strace 有时候还是会改变进程的行为的。这种时候更适合用 sysdig。Arch 刚刚更新的 sysdig 版本已经修正了崩溃的问题了~不过 Vim 对 sysdig 的输出就不像 strace 那样有好看的语法着色了。

其实我当时用 systemtap 来看信号发送情况更方便一些。不过那个需要内核调试符号,几百M的东西,装起来累啊……

Category: Linux | Tags: linux 终端 Python zsh
6
12
2016
0

SIGHUP, nohup, disown 以及 expect + sudo + bash + ssh

这些东西都和终端消失的时候(比如 ssh 连接中断)有关,但是细节上又各有不同。

nohup,去 coreutils 看源码就知道,是忽略 SIGHUP 然后 exec 相应的命令。信号处理器如果被设置成忽略,那么在 exec 之后依旧是忽略(与设置成用户自定义函数的情况不一样)。man 7 signal 可以看到说明。

disown 这个,我去看 zsh 的源码了。然而只看到 zsh 把进程从它的任务列表里删掉了,根本没提 SIGHUP 的事情。后来看到这个答案才知道原来 shell 也会发 SIGHUP 信号。

当然内核也会发 SIGHUP。查阅 drivers/tty/tty_io.c 可知,内核会给 session leader 及其组发 SIGHUP 和 SIGCONT,也会给前台进程组发,但是不会给后台进程组发 SIGHUP。那个是 shell 发的,所以 disown 之后后台进程就不会被 SIGHUP 干掉了。

所以,前台进程组如果没有被信号杀掉的话,会收到两次 SIGHUP 信号,一次 SIGCONT 信号。而后台进程组只会收到一次 SIGHUP。disown 过的不会收到任何信号。当然那些没死的进程,如果去读写终端,还是会得到 EIO 错误,写的时候还会收到 SIGPIPE 信号。

strace 可以观察到这些过程。

最后一个,出了问题。通过 expect 调用 sudo,然后登录服务器。终端断开时,expect 被 SIGHUP 杀死。sudo 会把用户发的信号传给它的子进程,但是内核发的不传。而 zsh 给 sudo 发信号时会因为权限原因而发送失败。于是后边的 bash 和 ssh 都会收不到 SIGHUP 信号。但是终端消失它们是能感知到的,所以这个出问题的进程树才这么深嘛。ssh 发现终端消失了,它干了什么呢?当然是通知对端终端没啦,然后等回复。对端 sshd 收到消息之后说,「哦哦,我去把 /dev/ptmx 给关掉。」于是 sshd 关掉了三个 /dev/ptmx 中的其中一个。所以这个 sshd 下的 bash 进程并不会得到 EIO 错误,还继续跑着。于是 sshd 还继续等着它跑完。于是这边的 ssh 还在等对端的 sshd 回复。于是这棵进程树就 hang 在这里了……

结论:还是 systemd 好啊,会话关闭时直接干掉这个会话启动的所有进程。需要在会话结束之后依旧运行的,自己用 systemd-run --user --scope xxx 启动就好。留下这些 sudo、ssh、bash 的还好,占用的资源不多。supervisorctl 这种的就囧了,死循环地读 stdin 又读不到东西,浪费一颗 CPU。

Category: Linux | Tags: linux 终端

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com