4
19
2011
3

Vim的Python3有内存泄漏?继续修正!

给Vim的Python3支持打了个补丁,发到邮件列表上只有Bram表示希望有人来测试就没有下方了。于是,这么久了,这个补丁的内存泄漏问题一直未被发现,直到看到蓝色基因的这篇文章。花了一个下午,发现我原来的补丁不仅没有修正本来就有的内存泄漏,反而雪上加霜,浪费了更多的内存。现在终于弄好了,放在我的陈列室里了,同时还莫名其妙地修正了另一个小问题

既然是内存泄漏,我首先想到的是valgrind这个工具。于是跑了一下:

valgrind --leak-check=full --show-reachable=yes vim

在开启的 Vim 中我 source 了蓝色基因的测试脚本:

lcd %:h
tabedit tmpbuffer
setlocal buftype=nofile
 
python3 << EOF
for i in range(3):
    flines= ['x'*200] * 50000
    vim.command("%s+\\_.*++g")
    for fl in flines:
        vim.current.buffer.append(fl)
    del flines[:]
EOF

整个过程CPU占到100%,而且运行速度极慢,内存消耗也非常多。最后Vim终于按我的指令退出时,valgrind刷屏了大约十几秒钟!而其间我看到除了Python的字样外,还有不少rb的字样。难道Ruby支持也有类似的问题?不过我不管它。重新编译了个只有--enable-python3interp选项的 Vim,这回跑起来快了一些,也没有那么多不相干的内存泄漏了。我也学聪明了点,把信息重定向到文件:

valgrind  --leak-check=full --show-reachable=yes src/vim 2> log

这样可以方便地在log中找“if_py”字符串了。可惜我弄的时候没想到自己会来写博客,所以log文件并没有保存。。。

首先我找到了DoPy3Command这个函数,valgrind说它里面分配的内存没有被释放。这里边的PyUnicode_AsEncodedString这块是我加的:

    /* PyRun_SimpleString expects a UTF-8 string. Wrong encoding may cause
     * SyntaxError (unicode error). */
    cmdstr = PyUnicode_Decode(cmd, strlen(cmd), p_enc, NULL);
    PyRun_SimpleString(PyBytes_AsString(PyUnicode_AsEncodedString(cmdstr, "utf-8", NULL)));

然后我能怎么办呢?当然是查Python的文档了。于是注意到文档上说PyUnicode_AsEncodedString返回的是新的引用。又去看官方教程上的示例,才知道如果一个API返回了新的引用,那么用完后应当手动Py_XDECREF!就像是strdup函数,它内部帮你malloc了,你自己用完后要记着free掉。(Py_XDECREFPy_DECREF的差别是,前者可以传NULL。)

于是就改吧,所有通过PyUnicode_AsEncodedString得到的对象都要Py_XDECREF下。为此,不仅需要临时变量来存储这个对象,更让我郁闷的是,在两个Python版本共有的函数StringToLine中有这样一段代码:

    str = PyString_AsString(bytes);
    len = PyString_Size(bytes);

这里的两个函数/宏我之前是这样定义的:

#define _PyUnicode_AsBytes(obj) PyUnicode_AsEncodedString(obj, p_enc, NULL)
#define PyString_AsString(obj) PyBytes_AsString(_PyUnicode_AsBytes(obj))
#define PyString_Size(obj) PyBytes_GET_SIZE(_PyUnicode_AsBytes(obj))

这下我没辙了,只好又改了if_py_both.hif_python.c文件,加了两个宏:PyString_AsBytesPyString_FreeBytes。它们在 Python2 的代码中什么也不做,但是在 Python3 的代码中用来保存和释放中间对象:

#define PyString_AsBytes(obj) PyUnicode_AsEncodedString(obj, p_enc, NULL);
#define PyString_FreeBytes(obj) Py_XDECREF(bytes)
#define PyString_AsString(obj) PyBytes_AsString(obj)
#define PyString_Size(obj) PyBytes_GET_SIZE(bytes)

有人说,if it ain't broken, don't fix it。可是,虽然问题只出在 Python3 部分,我还是得改 Python2 部分,感觉很不爽。

这样改完,再次反复运行测试代码,结果不遂人愿,依旧泄漏了不少内存。于是继续valgrind,又找到这里:

    static void
BufferDestructor(PyObject *self)
{
    BufferObject *this = (BufferObject *)(self);

    if (this->buf && this->buf != INVALID_BUFFER_VALUE)
	this->buf->b_python3_ref = NULL;
}

然后再次查教程中的示例:

static void
Noddy_dealloc(Noddy* self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject*)self);
}

再看看 Python2 部分的代码,在相应的函数里有Py_DECREF,于是把这示例的最后一行给BufferDestructor以及WindowDestructorRangeDestructor加上。再测试,内存不再消耗100多M了,反复source也不会继续增加,于是作出结论:Vim 的 Python3 支持部分没有已知的 bug 了!

做完这一切,我只想说:Vim 这 Python3 支持也太 broken 了吧,中文经常乱码就算了,vim.error不能用我也忍了,竟然还内存泄漏!难道写这个代码的人也是初学Python C API啊?

不过抱怨归抱怨,还是很感谢原作者的,不然我连修正都不可能。不过,patch 弄好也提交了,却一直没人理我,原作者难道是一时兴起才写的、然后就消失了?

最后,补丁现在放到陈列室了。

Category: python | Tags: vim python C代码
3
4
2011
4

Python3.2mu 与 Vim

曾经,我辛苦两星期自以为终于弄好了 Vim 的 +python3 特性,却未曾想到,编译新发布的安装 Python3.2 后 Vim 的 Python3 支持再次悲剧……

事情是这样的。在vim-cn群有人编译 Python3.2 出错问我。我于是把之前为尝新鲜而 make 的 Python3.2 又 make install 了。然后 ./configure 时就出问题了。具体错误不记得了,反正是找不到什么文件。后来我找出了我以前写的一个从 C 调用 Python 代码的小程序,编译通过,链接时找不到某些符号。折腾了好久,才知道是 Python3.2 的安装出错了,./configure 时要加 --enable-shared 参数。当然,我还比较习惯加上 --with-wide-unicode 参数。

于是我的 C 小程序编译运行成功。但 Vim 的依旧悲剧。看了 src/configure.in,注意到它并没有使用 pkg-config,而是按以前 Python 的头文件和库文件的规律硬编码进去的。这时我才发现 Python 的相关文件/目录都多了个 mu 后缀:

>>> pkg-config --cflags --libs python-3.2
-I/usr/local/include/python3.2mu  -L/usr/local/lib -lpython3.2mu
>>> ls -li /usr/local/bin/python*
163890 -rwxr-xr-x 3 root root 10877 2011-03-01 23:16 /usr/local/bin/python3
163890 -rwxr-xr-x 3 root root 10877 2011-03-01 23:16 /usr/local/bin/python3.2
164216 lrwxrwxrwx 1 root root    18 2011-03-01 23:18 /usr/local/bin/python3.2-config -> python3.2mu-config
163890 -rwxr-xr-x 3 root root 10877 2011-03-01 23:16 /usr/local/bin/python3.2mu
164107 -rwxr-xr-x 1 root root  1827 2011-03-01 23:18 /usr/local/bin/python3.2mu-config
164252 lrwxrwxrwx 1 root root    16 2011-03-01 23:18 /usr/local/bin/python3-config -> python3.2-config

这个 mu 后缀是什么意思呢?搜了半天,终于找到了:m 是普通版,u 是宽字符版(--with-wide-unicode),还有个 d 表示使用了 --with-pydebug 参数编译的。加了这些后缀,于是 Vim 配置脚本的硬编码就失败了。(它为什么要硬编码呢……T.T)对于 mu 版,修改方法是这样的:

# For Python3.2
if which python3 >/dev/null 2>&1 && [ $(python3 -c 'import sys; print(sys.version_info.minor)') -ge 2 ]; then
  sed -i -e 's|-lpython${vi_cv_var_python3_version}[dmu]*|-lpython${vi_cv_var_python3_version}mu|' \
         -e 's|python${vi_cv_var_python3_version}/config[^"]*|python${vi_cv_var_python3_version}/config-3.2mu|' \
         -e 's|include/python${vi_cv_var_python3_version}[dmu]*|include/python${vi_cv_var_python3_version}mu|' \
    src/configure.in
  # Fixed: no longer needed.
  # sed -i -e 's|PyEval_InitThreads();|/* PyEval_InitThreads(); */|' \
  #   src/if_python3.c
  autoconf=1
fi

[ $autoconf -eq 1 ] && (cd src && autoconf)

后面那个对 src/if_python3.c 的修改我也不知道是为什么,反正不这样的话调用 Python 时就 SIGABRT 出错退出,而这样改了之后好像也没什么负面影响。至于找出这个语句的办法嘛,当然是不知比 jdb 好用多少倍的 gdb 啰。


2011年4月19日更新修正了 Python3 接口的内存泄漏问题,发现已不再需要删掉那句代码了(删掉后反而出错)。

Category: python | Tags: python vim 编译
2
22
2011
3

Vim7.3 的 Python3 支持修正补丁

Vim 7.3 增加了对 Python3 的支持,但其有不少 bug,从不能正确地向缓冲区中添加中文文本,到 buffer 对象不支持 slice 操作,vim.error 不是 BaseException 的子类而是一个 str,以及各种中文乱码/UnicodeDecodeError,让我这个 Python3 的坚定支持者非常郁闷,于是趁假期把 Vim 好好修理了一番。

此次修正历时两周,涉及 src/if_{py_both.h,python.c,python3.c},共 3 files changed, 308 insertions(+), 265 deletions(-),修正的具体项目为

  • 向缓冲区添加文本时正确处理编码
  • buffer 对象支持 slice 赋值
  • vim.error 不再是字符串
  • py3file 让 Python 检测文件编码
  • 向 Python 传递缓冲区字符串时使用正确的编码(这解决了 gundo 在非 UTF-8 编码时的解码出错)
  • py3 命令输入使用 'encoding' 解码后再以 UTF-8 编码(这解决了在 'encoding' 非 UTF-8 时含中文的 py3 命令的 SyntaxError)
  • 向标准输出写文本时使用正确的编码(这样 print() 之类不会输出乱码)

以上数据要感谢git工具。

目前我在 Ubuntu Linux 10.10 32bit 和 Windows XP SP3 (使用 MinGW 编译)上测试没有问题,有兴趣的请帮忙再测试下。下载地址

另外,附上 gundo 的 Python 2 & 3 兼容版以及使用 Python3 支持的 Python 补全插件 python3complete.vim(放到 ~/.vim/autoload 下)。


PS:Python3.2 发布了,增加了一些很好的新特性,比如argparse模块,str.format_map方法等等。


2011年3月3日更新:给 Vim 编译 Python3.2 支持也有些艰难,参见这篇文章

2011年4月19日更新:今天解决了内存泄漏的问题,补丁已更新。另外,本补丁将在陈列室维护。

2011年6月19日更新:此补丁已被官方采纳。

Category: Vim | Tags: C代码 python vim
12
15
2010
4

从UDP到解决DNS污染的脚本

最近做网络课的实验,涉及UDP协议。因为UDP协议比TCP用得少,所以我以前没试过创建几个UDP的socket。现在忽然有了兴致,就试了试。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

socket创建好了,往哪里发消息呢?我想到了DNS。首先要有报文。没找到容易上手的Python库,就Google到了这个,DNS报文的格式。没功夫细细研究,先弄个报文测试下:

q = b'>:\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07twitter\x03com\x00\x00\x01\x00\x01'
s.sendto(q, ('8.8.8.8', 53))

\x07twitter\x03com\x00就是要查询的域名了,我想测试什么,明白人都看得出来了。接下来接收回答,接收了多次:

>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'46.82.174.68'
>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'128.121.146.100'

再接收的话就阻塞了。对于普通域名,当然是最多只会接收到一次的啦。如果报文出错的话(比如我不小心少写了最后的\x00\x01),是收不到回答的。

有人依据此现象写了个pydnsproxy 放在Google Code上。主页说“暂不公布方法”,但其实很简单。下面是我整理过的代码,转成了Python3的语法:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

from socketserver import *
from socket import *
import sys, os

'''
来源 http://code.google.com/p/pydnsproxy/
'''

DEF_LOCAL_HOST = '127.0.0.1'
DEF_REMOTE_SERVER = '8.8.8.8'
DEF_PORT = 5350
DEF_CONF_FILE = 'dnsserver.conf'
DEF_TIMEOUT = 0.4

gl_remote_server = None

class LocalDNSHandler(BaseRequestHandler):
  def setup(self):
    global gl_remote_server
    if not gl_remote_server:
      remote_server = DEF_REMOTE_SERVER
    else:
      remote_server = gl_remote_server
    self.dnsserver = (remote_server, 53)

  def handle(self):
    data, socket = self.request
    rspdata = self._getResponse(data)
    socket.sendto(rspdata, self.client_address)

  def _getResponse(self, data):
    "Send client's DNS request (data) to remote DNS server, and return its response."
    sock = socket(AF_INET, SOCK_DGRAM) # socket for the remote DNS server
    sock.sendto(data, self.dnsserver)
    sock.settimeout(5)
    while True:
      try:
        rspdata = sock.recv(4096)
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Try again")
    # "delicious food" for GFW:
    while True:
      sock.settimeout(DEF_TIMEOUT)
      try:
        rspdata = sock.recv(4096)
        print("GFWed?")
      except timeout:
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Trying again")
    return rspdata

class LocalDNSServer(ThreadingUDPServer):
  pass

def main():
  global gl_remote_server
  try:
    if hasattr(sys, 'frozen'):
      dir = os.path.dirname(sys.executable)
    else:
      dir = os.path.dirname(__file__)
    confFile = os.path.join(dir, DEF_CONF_FILE)
    f = open(confFile, 'r')
    dns = f.read().split('=')
    f.close()
    if len(dns) == 2:
      if dns[0].strip().lower() == 'dns':
        gl_remote_server = dns[1].strip()
      else:
        pass
  except:
    pass
  dnsserver = LocalDNSServer((DEF_LOCAL_HOST, DEF_PORT), LocalDNSHandler)
  dnsserver.serve_forever()

if __name__ == '__main__':
  main()

注意到和原程序不同的是,我捕获了错误号为11的socket.error异常。这个是EAGAIN,“资源临时不可用”,只会在设置了超时后出现。man文档recv(2)对此的解释是:

EAGAIN or EWOULDBLOCK
       The socket is marked  nonblocking  and  the  receive  operation
       would  block, or a receive timeout had been set and the timeout
       expired before data was received.  POSIX.1-2001  allows  either
       error  to be returned for this case, and does not require these
       constants to have the same value,  so  a  portable  application
       should check for both possibilities.

不知道是怎么回事,再次接收却又可以收到数据。也许正如其名,是期待调用者再次尝试吧。我越来越觉得,Python 的异常处理里应该有tryagain这样的语句了。

2012年11月10日更新:更好地摆脱 DNS 污染,请参考此文

Category: 网络 | Tags: DNS python 网络 UDP
10
29
2010
7

金山快盘之跨平台同步脚本

金山快盘是和Dropbox差不多的同步服务,区别只是快盘(目前)只支持Windows,容量只有2G多(我的Dropbox都4G多了~),安全性不好(网页版的使用不加密的HTTP协议)、同步文件夹被设为系统属性等等。

由于某些原因,同学和我通过金山快盘分享文件。于是开始研究之。客户端上传下载时使用XML格式,但数据都加密过了,无解。于是转攻网页版。基本上都是很简单的json,上传使用了个Flash,用Wireshark抓包一看还是HTTP POST。

于是ksync诞生了,仅支持从服务器到本机的单向共享文件同步。如有其它需求可自行扩展。本程序仅需要Python3支持,目前仅在Ubuntu Linux上测试通过。

下载链接。使用前记得编辑ksync文件,把自己的用户名、cookie文件存放路径、同步目标路径替换掉。

Category: python | Tags: linux python json
10
21
2010
6

Python HTTP 请求时对重定向中的 cookie 的处理

首先说明一下,我使用的是 Python3 的 urllib,但 Python2.x 同理(使用 urllib2)。

想用脚本去登录一个网站。和很多网站一样,该网站使用 cookie 来保存会话信息。这个我以前是自己提取 response 中的 Set-Cookie 头来处理的。这次本想如法炮制,却发现没保存需要的 cookie,所以登录失败。

很郁闷地想了半天,最后出去 wireshark 抓包,终于发现原来重要的 cookie 在登录后的应答中,但这个应答是个 302 重定向,所以 urllib 默认的 opener (urllib.request.urlopen)直接就跟从这个重定向了,没有对 cookie 进行任何处理。

我首先想到的是,不要跟从重定向。我看到有个 HTTPRedirectHandler,但文档里没写它怎么用。郁闷……自己找到 request.py 文件看源代码,折腾了好久无果,遂想到 Google (早该想到了。。。)于是找到了 StackOverflow 上。有两个解决办法:要么不跟从重定向,要么弄个 HTTPCookieProcessor 保存 cookie 信息。看我自己的需求,当然选后者了。而且,那个回答问题的人也没有给出如何不让它跟从重定向(所给代码只是在重定向前对 cookie 进行处理而已)。

于是,我再一次地打开了 http.cookiejar 的文档,尝试弄明白这东西到底怎么用。当初折腾 cookie 的时候,没弄明白这个,所以才自己处理的。

看 request.py 里的代码,这个 CookieJar 用起来相当不错:

class HTTPCookieProcessor(BaseHandler):
    def __init__(self, cookiejar=None):
        import http.cookiejar
        if cookiejar is None:
            cookiejar = http.cookiejar.CookieJar()
        self.cookiejar = cookiejar

    def http_request(self, request):
        self.cookiejar.add_cookie_header(request)
        return request

    def http_response(self, request, response):
        self.cookiejar.extract_cookies(response, request)
        return response

    https_request = http_request
    https_response = http_response

不过我需要将 cookie 信息保存到文件。从文档上看到有个 FileCookieJar。我尝试了下,出错了,没有 _really_load 方法,我晕。。。之后才注意到其源代码开头有个ASCII图:

						CookieJar____
                        /     \      \
            FileCookieJar      \      \
             /    |   \         \      \
 MozillaCookieJar | LWPCookieJar \      \
                  |               |      \
                  |   ---MSIEBase |       \
                  |  /      |     |        \
                  | /   MSIEDBCookieJar BSDDBCookieJar
                  |/
               MSIECookieJar

原来具体实现还在子类啊。好吧,我就用 MozillaCookieJar 好了。

用法很简单,初始化时把文件名传给它,载入用 load(),保存用 save()。不过要注意的是,文件不存在时不能载入,touch 个空文件出来也不行的。

另外,那个 StackOverflow 的页面还提到了 mechanize 这个模块,有时间去尝试下 :-)

最后,如果我不要它重定向该怎么做呢?难道非要我去用更底层的 http.client?

Category: python | Tags: python 网络

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