11
10
2013
5

终止永远等待网络的程序——纠结的 getmail 不再纠结

我收取邮件一直用的是 getmail,然而它有个问题:在网络不好的时候会挂在 recv 系统调用上,等好几个小时都有可能。还好我用的 crond 是 dcron,它知道同一个任务,在上一次任务还没执行完时即使时间到了也不应该再次执行,省了我一堆 flock 锁。不过,这样子导致我收不到邮件也不行啊。

以前也研究过一次,看到 getmail 有设置 socket 的超时时间啊,没整明白。最近网络又老是抽风,而且相当严重,导致我得不断地用 htop 去看、去杀没有反应的 getmail 进程。烦了,于是一边阅读 getmail 源码,一边使用 strace 观察,再配合 iptables 这神器,以及 the Silver searcher,终于找到了问题所在。

原来,由于 Python 的 SSL 对非阻塞套接字的支持问题12,getmail 在使用 SSL 连接时会强制使用阻塞式的套接字(见代码getmailcore/_pop3ssl.py:39以及getmailcore/_retrieverbases.py:187)。也许 Python 2.7 已经解决了这个问题,但是看上去 getmail 还是比较关心 Python 2.3 和 2.4。不过就算是 SSL 支持不好,调用下alarm不要一直待在那里傻傻地等嘛……也许,大部分 getmail 用户很少遇到足够差的网络?

于是考虑 fetchmail。花了两三天的业余时间终于弄明白我的需求该怎么配置了:

set daemon 300
set logfile ~/etc/log/fetchmail.log

defaults proto pop3 timeout 120 uidl
keep fetchsizelimit 0 mda "procmail -f %T"

poll pop.163.com interval 2
username "username" password "password"

poll pop.gmail.com
username "username" password "password"
ssl

poll pop.qq.com interval 2016 # 7 days
username "username" password "password"

但结果就是,除了 GMail 好一点,我让它「对从现在起所收到的邮件启用 POP」就没太大问题之外,腾讯还好,没几封邮件。网易那边,几百封旧邮件全部拖回来了…………

其实这个问题也还好,毕竟是一次性的。可我看它的日志,又发现,它每次收到 GMail 时,都会打印有多少封邮件已读。难道说,它每次去收邮件时都要列出所有可以用 POP3 收取的邮件,然后挑出没有收取过的?想到如果是这样,以后它每次取邮件时都要先取几千上万条已读邮件列表……这不跟 Google App Engine SDK 操作数据库加 offset 时前边所有数据全部读一遍一样扯淡吗……

于是又回来折腾 getmail。其实就这么一个问题,解决了就好。本来是准备去学学ptrace怎么用的,结果忍不住了,直接拿 Python 调 strace 写了这个:

#!/usr/bin/env python3

'''wait and kill subprocess if it doesn't response (from network)'''

import os
import sys
import select
import tempfile
import subprocess

timeout = 60

def new_group():
  os.setpgrp()

def main(args):
  path = os.path.join('/dev/shm', '_'.join(args).replace('/', '-'))
  if not os.path.exists(path):
    os.mkfifo(path, 0o600)
  pipe = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
  p = subprocess.Popen(['strace', '-o', path, '-e', 'trace=network'] + args, preexec_fn=new_group)

  try:
    while True:
      ret = p.poll()
      if ret is not None:
        return ret
      rs, ws, xs = select.select([pipe], (), (), timeout)
      if not rs:
        print('subprocess met network problem, killing...', file=sys.stderr)
        os.kill(-p.pid, 15)
      else:
        os.read(pipe, 1024)
  except KeyboardInterrupt:
    os.kill(-p.pid, 15)

  return -1

if __name__ == '__main__':
  try:
    import setproctitle
    setproctitle.setproctitle('killhung')
    del setproctitle
  except ImportError:
    pass
  sys.exit(main(sys.argv[1:]))

Python 果然快准狠 ^_^

代码在 winterpy 仓库里也有一份

这还是我编程时第一次用到进程组呢。没办法,光杀 strace 进程没效果。嗯,还有非阻塞的命名管道


PS: 去 GMail 设置页看完那个选项的具体名字后离开,结果遇到这个:

你这是让我「确定更改」呢还是「取消取消更改」呢……

| Theme: Aeros 2.0 by TheBuckmaker.com