12
22
2016
9

利用 systemd 的 watchdog 功能重启卡住的服务

我在用 offlineimap。用着用着就发现一个问题:偶尔 offlineimap 会卡在网络上不动弹了。跟 getmail 一个德性……

但是 offlineimap 又跟 getmail 有点不一样,它是持续运行着的。虽然非要把之前那个 killhung 程序拿来用不是不可以,但我还是重新弄了一个更优雅的方案:systemd watchdog。

我的 offlineimap 本来就是用 systemd 服务的方式来跑的,所以很适合这样的改造呢。只是,当我瞅了一眼源码之后,我就放弃了 patch offlineimap 的打算。很难在合适的地方添加 watchdog 相关的代码。

既然从内部着手不好做,那就从外部写一个 wrapper 好了,反正 offlineimap 跟 getmail 不一样,正常情况下一直在输出东西,就把这个作为它的「心跳」特征好了。当然这个 wrapper 还可以给其它程序用。

于是,watchoutput 程序诞生了!稍微改一下 offlineimap 的 .service 文件,像这样子就好了:

[Unit]
Description=Offlineimap Service

[Service]
Type=notify
ExecStart=.../watchoutput /usr/bin/offlineimap
TimeoutStopSec=3s
SyslogIdentifier=offlineimap
Restart=on-failure
WatchdogSec=70
LimitCORE=0

[Install]
WantedBy=default.target

加上LimitCORE=0是为了阻止重启的时候由于 SIGABRT 信号导致 coredump,浪费磁盘空间。

用了几天之后,终于观察到一次由 watchdog 触发的重启:

12月 19 12:26:53 lilywork offlineimap[21623]:  Establishing connection to imap.exmail.qq.com:993 (main-remote)
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Watchdog timeout (limit 1min 10s)!
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Killing process 21623 (python3) with signal SIGABRT.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Killing process 21625 (offlineimap) with signal SIGABRT.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Main process exited, code=dumped, status=6/ABRT
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Unit entered failed state.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Failed with result 'core-dump'.
12月 19 12:28:03 lilywork systemd[687]: offlineimap.service: Service hold-off time over, scheduling restart.
12月 19 12:28:03 lilywork systemd[687]: Stopped Offlineimap Service.
12月 19 12:28:03 lilywork systemd[687]: Starting Offlineimap Service...
12月 19 12:28:04 lilywork systemd[687]: Started Offlineimap Service.

没过几天,我又给这个 watchoutput 的脚本找到另外的用处:自动重启网络。

我家里的笔记本连 Wi-Fi 不知怎么,这些天经常会卡住(只发不收,一直处于 ARP 找网关的状态)。内核之前报过一次错,现在也没反应了。

于是:

[Unit]
Description=Watch for network availability

[Service]
Type=notify
ExecStart=.../watchoutput --retry-on-exit 2 --wait-before-retry 30 --ignore-stderr \
    -- ping -i 30 192.168.1.1
Restart=on-failure
WatchdogSec=70
StandardOutput=null
StandardError=journal
LimitCORE=0
SyslogIdentifier=watch-network

[Install]
WantedBy=default.target

拿 watchoutput 监控 ping 网关的输出,每30秒 ping 一次,如果70秒还没反应就重启它自己。然后我们还需要重新连接网络。在 /etc/systemd/system 下建立 netctl-auto@wlan0.service.d 目录,并在其下建立一个 watchdog.conf 文件,给 netctl-auto@wlan0.service 服务增加一项配置:

[Unit]
PartOf=watch-network.service

这样当 watch-network.service 重启的时候,netctl-auto@wlan0.service 就会自动重启了~

Category: Linux | Tags: linux systemd
12
14
2016
0

使用 RSS 订阅知乎用户的动态

之前做了一个知乎专栏转RSS的网关,这次又写了个针对知乎动态的。感兴趣的话去 https://rss.lilydjwg.me/ 看看用法吧。

此功能只支持用户的回答和发布文章两个操作。别的操作,比如赞了答案啦,关注了专栏啦,参加了 live 啦,没有实质性的内容,又可能会有非常多,不适合 RSS 这种面向内容发布的东西,所以就过滤掉了。即使如此,程序每次访问取最近40条动态,缓存有好几个小时,所以对于活跃的用户是可能漏掉一些动态的。反正现在信息这么多,漏了就漏了吧,去读读别的东西呗。

知乎这次暴露出来的 API 有点意思。它有一个 include 参数,看起来是指定要返回哪些字段的。看起来知乎也在解决 GitHub 遇到的同样的问题:RESTful API 太不灵活了。只是,为什么要造轮子啊,GraphQL 不是挺好的吗……

Category: 网络 | Tags: rss 知乎
11
10
2016
14

数据让 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
16

诡异多多的 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: linux iptables UDP

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