7
31
2015
10

交换 ThinkPad 键盘上的 Insert 和 End 键

ThinkPad 键盘上的第一行键现在默认在不按下Fn键时执行多媒体按键功能,按下Fn时才能执行F1-F12的功能,对于像我这种 Vimhtop 等的用户来说颇为不便。还好,BIOS 选项里可以改回来。

然后我遇到了 X250,发现InsertEnd键怎么也受那个选项的影响了!也就是,如果F1-F12设置得方便了,那么End键就需要Fn键配合。而HomeEnd这种光标移动键虽然很少用,但毕竟还是要用到的,比如在 htop、weechat、mutt 以及不支持自定义编辑键的 Qt 程序里的时候。

所以呢,我在~/.Xmodmap里把这两个键交换了:

keysym End = Insert
keysym Insert = End

这下子用笔记本上的键盘是没问题了。可是我用外接键盘的话,这两个键就又反过来了 Orz……

当然网上会有 udev 规则,在插上外设时跑个脚本什么的。可不管怎么映射,总有个键盘的按键是反的啊

仔细询问 Google 之后,在 Gentoo 的论坛里终于发现这么一条线索

To alter keymap of a particular keyboard you need to issue EVIOCSKEYCODE ioctl on corresponding /dev/input/eventX node.

所以,找到相应的 event 设备文件之后,只需要 ioctl 一下就可以了?可是EVIOCSKEYCODE是个什么鬼啊……

找过 manpages、头文件、内核文档、Google 之后,我不得不相信这个东西真的没文档!于是只好看源码了……还好有 LXR,不用在本地近一个G的源码里搜索。

所以,翻完文档又试验,最终有了这个程序:

#include<sys/ioctl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<linux/input.h>

#define EVENT_FILE "/dev/input/by-path/platform-i8042-serio-0-event-kbd"

int main(int argc, char **argv){
  unsigned int codes[][2] = {
    {0xd2, 107}, //Insert -> End
    {0xcf, 110}, //End -> Insert
    {0, 0},
  };
  int fd = open(EVENT_FILE, O_RDONLY);
  if(fd < 0) {
    perror("open " EVENT_FILE);
    exit(1);
  }

  unsigned int (*p)[2];
  for(p=codes; *p[0]; p++){
    if(ioctl(fd, EVIOCSKEYCODE, *p)) {
      perror("ioctl EVIOCSKEYCODE");
      exit(1);
    }
  }
  return 0;
}

那个EVENT_FILE当然就是笔记本键盘的节点啦,在/dev/input/by-path下很容易识别的。

编译之后,每次启动系统后执行一次就可以了(大概)。

对了,顺便说一下,找那些代码可以用 showkey 和 getkeycodes 之类的命令。当然我们有 setkeycodes,但是它不能为指定键盘单独设置。传进去的参数,第一个整数是 scancode,就是硬件上报的编码,第二个是 keycode,内核给键的编码,不同硬件的不同 scancode 可以对应同一个 keycode(比如本文所做的)。然后文本终端还有 keymap、X Window 还有键盘布局和 xmodmap,大概是把 keycode 映射到可读的键名。

7
26
2015
1

一个简单的 zsh 模块

曾经,我让 Awesome 收养孤儿进程,以保持一个清晰的进程树。后来我又想让 zsh 也做这个 prctl 系统调用,免得子进程 fork 之后跑太远。比如 Wine 跑起来就好多个服务进程,如果不能把它们全部关掉的话,再启动另一个版本的 Wine 会出问题的。而当我启动好些个不同版本的 Wine 环境之后,只看到 Awesome 下边挂了一堆 Wine 的进程,却不知道哪些属于哪个 Wine 环境的了。

zsh 本身并不支持做这个调用,不过如同 Python 和 Lua 一样,zsh 也可以通过共享库来扩展功能。不同的是,zsh 模块是没有文档的……好在 zsh 源码里提供了一个 example 示例模块。把它改改就有了以下代码:

把这两个文档保存到 zsh 源码目录的Src/Modules下,可能还需要编辑一下config.modules文件,然后编译就可得 subreap.so 文件。把这个文件放到/usr/lib/zsh/$ZSH_VERSION/zsh/subreap.so然后就可以用了:

zmodload zsh/subreap
subreap

模块加载之后,多了个subreap内建命令。不带参数即调用prctl(PR_SET_CHILD_SUBREAPER, 1),这样不管其子进程怎么 fork,都会在此 zsh 的进程树之下。使用subreap -u来取消这个设置。

如果你不想编译而又是 Linux 64 位系统,可以试试我编译好的版本:下载地址, 签名, SHA1: 09eb1cc9ebf6ec1e681641c0a60f57425cbb1e8c。

Category: Linux | Tags: linux zsh C代码
6
1
2014
11

给 Python 加上分数和十进制数字面量表示

听说 Ruby 支持分数字面量呢——

irb(main):001:0> 1/3r
=> (1/3)
irb(main):002:0> 1/3r+1/2r
=> (5/6)

可是在 Python 里只能这样子:

>>> from fractions import Fraction as R
>>> R(1, 3)
Fraction(1, 3)
>>> R(1, 3) + R(1, 2)
Fraction(5, 6)

学习《用 `accio` 代替 `import`:深入理解自举和 Python 语法》改改 Python 解释器,让它也支持分数字面量,应该会很有趣呢 =w=

去翻了一下 Grammar/Grammar 文件,有些失望。那里只有语法定义,没有词法定义呢。以为词法定义在另一个地方,所以找了找,无果。只有 C 代码。想到复数的 j 字尾,直接拿 ag 搜带引号的 "j"'j',最终确认它在 Parser/tokenizer.c 里。也就是说,Python 的词法分析是自己实现的。

在那个四千多行的tok_get函数里,有一部分代码在尝试解析一个数,也就是语法里的 NUMBER。照着解析复数的办法,把 d 后缀和 r 后缀也加进去:

diff -r bf65e7db066d Parser/tokenizer.c
--- a/Parser/tokenizer.c    Mon Apr 14 22:27:27 2014 -0400
+++ b/Parser/tokenizer.c    Fri May 30 20:12:07 2014 +0800
@@ -1528,6 +1528,10 @@
                 goto fraction;
             if (c == 'j' || c == 'J')
                 goto imaginary;
+            if (c == 'd' || c == 'D')
+                goto decimal;
+            if (c == 'r' || c == 'R')
+                goto rational;
             if (c == 'x' || c == 'X') {

                 /* Hex */
@@ -1621,6 +1625,12 @@
                     /* Imaginary part */
         imaginary:
                     c = tok_nextc(tok);
+                else if (c == 'd' || c == 'D')
+        decimal:
+                    c = tok_nextc(tok);
+                else if (c == 'r' || c == 'R')
+        rational:
+                    c = tok_nextc(tok);
             }
         }
         tok_backup(tok, c);

d 后缀是我给十进制数——就是会计里会用到的精确的十进制小数——准备的。

然后可以编译出来试试。这个 configure 命令是从 Arch 官方编译脚本里找的。

./configure --enable-shared --with-threads --with-computed-gotos --enable-ipv6 --with-valgrind --with-system-expat --with-dbmliborder=gdbm:ndbm --with-system-ffi --with-system-libmpdec --without-ensurepip
make

因为我不执行安装步骤,而又用了共享库,所以要这样子执行:

LD_LIBRARY_PATH=. ./python

试试看:

>>> 4d
ValueError: could not convert string to float: 4d

有效果,不报语法错了呢。

现在报ValueError,因为我还没告诉 Python 如何解析我新加的字面量表示呢。解析代码位于Python/ast.cparsenumber函数。最终的补丁如下:

diff -r bf65e7db066d Python/ast.c
--- a/Python/ast.c  Mon Apr 14 22:27:27 2014 -0400
+++ b/Python/ast.c  Fri May 30 20:12:07 2014 +0800
@@ -3650,12 +3650,29 @@
     long x;
     double dx;
     Py_complex compl;
-    int imflag;
+    char typeflag;
+    PyObject *mod, *type, *ret;

     assert(s != NULL);
     errno = 0;
     end = s + strlen(s) - 1;
-    imflag = *end == 'j' || *end == 'J';
+    switch(*end){
+        case 'j':
+        case 'J':
+            typeflag = 'j';
+            break;
+        case 'd':
+        case 'D':
+            typeflag = 'd';
+            break;
+        case 'r':
+        case 'R':
+            typeflag = 'r';
+            break;
+        default:
+            typeflag = 'i';
+    }
+
     if (s[0] == '0') {
         x = (long) PyOS_strtoul(s, (char **)&end, 0);
         if (x < 0 && errno == 0) {
@@ -3670,13 +3687,43 @@
         return PyLong_FromLong(x);
     }
     /* XXX Huge floats may silently fail */
-    if (imflag) {
+    if (typeflag == 'j') {
         compl.real = 0.;
         compl.imag = PyOS_string_to_double(s, (char **)&end, NULL);
         if (compl.imag == -1.0 && PyErr_Occurred())
             return NULL;
         return PyComplex_FromCComplex(compl);
     }
+    else if (typeflag == 'd') {
+      mod = PyImport_ImportModule("decimal");
+      if (mod == NULL)
+          return NULL;
+
+      type = PyObject_GetAttrString(mod, "Decimal");
+      if (type == NULL) {
+          Py_DECREF(mod);
+          return NULL;
+      }
+      ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1);
+      Py_DECREF(type);
+      Py_DECREF(mod);
+      return ret;
+    }
+    else if (typeflag == 'r') {
+      mod = PyImport_ImportModule("fractions");
+      if (mod == NULL)
+          return NULL;
+
+      type = PyObject_GetAttrString(mod, "Fraction");
+      if (type == NULL) {
+          Py_DECREF(mod);
+          return NULL;
+      }
+      ret = PyObject_CallFunction(type, "s#", s, strlen(s)-1);
+      Py_DECREF(type);
+      Py_DECREF(mod);
+      return ret;
+    }
     else
     {
         dx = PyOS_string_to_double(s, NULL, NULL);

因为只是玩玩,所以不太认真,没仔细做错误处理;因为decimalfractions模块是从外部文件导入的,所以可能被覆盖掉,从而导致报错,并且这错误是无法通过异常处理捕获的。

不出问题的话,再次make之后,就可以开始玩了。不过在此之前,再多做几个补丁,让 Python 把分数和十进制数显示得简洁好看一点:

diff -r bf65e7db066d Lib/decimal.py
--- a/Lib/decimal.py    Mon Apr 14 22:27:27 2014 -0400
+++ b/Lib/decimal.py    Fri May 30 20:12:07 2014 +0800
@@ -1015,7 +1015,7 @@
     def __repr__(self):
         """Represents the number as an instance of Decimal."""
         # Invariant:  eval(repr(d)) == d
-        return "Decimal('%s')" % str(self)
+        return str(self) + 'd'

     def __str__(self, eng=False, context=None):
         """Return string representation of the number in scientific notation.
diff -r bf65e7db066d Lib/fractions.py
--- a/Lib/fractions.py  Mon Apr 14 22:27:27 2014 -0400
+++ b/Lib/fractions.py  Fri May 30 20:12:07 2014 +0800
@@ -280,7 +280,7 @@

     def __repr__(self):
         """repr(self)"""
-        return ('Fraction(%s, %s)' % (self._numerator, self._denominator))
+        return str(self) + 'r'

     def __str__(self):
         """str(self)"""
diff -r bf65e7db066d Modules/_decimal/_decimal.c
--- a/Modules/_decimal/_decimal.c   Mon Apr 14 22:27:27 2014 -0400
+++ b/Modules/_decimal/_decimal.c   Fri May 30 20:12:07 2014 +0800
@@ -3092,18 +3092,10 @@
 static PyObject *
 dec_repr(PyObject *dec)
 {
-    PyObject *res, *context;
-    char *cp;
-
-    CURRENT_CONTEXT(context);
-    cp = mpd_to_sci(MPD(dec), CtxCaps(context));
-    if (cp == NULL) {
-        PyErr_NoMemory();
-        return NULL;
-    }
-
-    res = PyUnicode_FromFormat("Decimal('%s')", cp);
-    mpd_free(cp);
+    PyObject *res, *str;
+    str = dec_str(dec);
+    res = PyUnicode_FromFormat("%Ud", str);
+    Py_DECREF(str);
     return res;
 }

下面是最终成果啦:

>>> 0.1 + 0.2 == 0.3
False
>>> 0.1d + 0.2d == 0.3d
True
>>> 1/3r + 1/2r
5/6r
>>> 0.4/1.2r
0.33333333333333337
>>> 0.4r/1.2r
1/3r

可以看到,与复数类似,分数字面量其实包含了一次除法。所以如果分子写浮点数的话,最终结果是会被转成浮点数的呢。这个和 Ruby 的行为是一样的 =w=

Category: python | Tags: Python C代码
3
2
2014
3

FUSE 初体验:Android dedupefs

自打知道 FUSE 以来都觉得亲手写一个 FUSE 文件系统是很好玩的事情,但是因为没好的自己能够很快实现的点子所以一直没动手。前段时间需要从 Android xrecovery 备份中取得一旧版本的应用,才决定动手的,顺便也练习一下很久没怎么用到的 C 语言。至于为什么不用 Python,好像那个 Python 绑定不太稳定的样子,Python 3 版更是如此。而且我也不希望效率太差。

首先介绍一下,所谓的「dedupefs」,就是把 Android xrecovery 的「dedupe」备份格式的数据挂载成文件系统来查看。其实仅仅只是想查看的话,把那个 dedupe 目录下的东东 gcc 一下就可以创建和解开 dedupe 的备份了,只是占用很多磁盘空间而已。

dedupe 的格式很简单,一个文本文件描述文件信息(时间、路径、大小、类型等),一个目录里全是 sha256 命名的文件来存储文件的数据,以便在备份时不同的备份中的相同文件只保存一次。

FUSE 嘛,我好像从来没看到过完整一点的文档,就是官方 API 文档也经常语焉不详。dedupefs 是参考 rofs 写的。dedupefs 也是只读的。

挂载之前,先得把 dedupe 的纯文本格式处理一下。纯文本适合存储和人阅读,但是查询效率低下。我决定用更适合处理纯文本的 Python,把数据存储到 GNU dbm 键值对数据库中,然后 dedupefs 直接读取数据库就好了。(于是顺便学会了在 C 中使用 GNU dbm :-))数据的组织方式如下:

  • d + 文件路径:该目录下的文件名列表
  • f + 文件路径:该文件的信息

这样要读取一个目录下的文件列表就查 d 开头的项,要取得一个文件的信息(stat)或者打开文件,就读 f 开头的。

下边是编码和调试过程中的经验与收获:

  • GNU dbm 没说它是线程安全的,所以它不是线程安全的。但是 FUSE 又是多线程的(调试用的单线程模式我就不玩的),所以读取数据库时要加锁。
  • GNU dbm 查询结果数据是要调用者来 free 的。
  • 因为涉及到二进制数据交换(Python <-> C),所以要注意在结构体声明时围上#pragma pack(push, 1)#pragma pack(pop),以免对齐不一致造成数据错误。
  • valgrind 用来诊断内存访问错误效果非常棒!
  • FUSE 的struct fuse_file_info里有个fh域可以用来存文件描述符,这样就不用像 rofs 那样每次读取都要打开一遍文件了。
  • FUSE 读取用的回调函数传的offset一定要用,要首先lseek(finfo->fh, offset, SEEK_SET);一下,不然指不定读取到什么地方的数据了。
  • FUSE 文件系统可以忽略文件权限,所以自己不在openaccess里判断的话,就可以访问到明明看上去不能访问的文件(这正在我想要的)。
  • du 命令读取文件占用磁盘空间时使用了struct statst_blocks域。如果在 FUSE 程序里不管它的话,那么 du 将总是报告占用了 0 字节的空间……这里的块大小总是 512 字节。

第一次写 FUSE 程序,虽然文档差了一点,但用起来还是挺方便 =w=

哦对了,android-dedupefs 的仓库链接。

2
23
2014
11

让我们收养孤儿进程吧

稍微了解一点类 UNIX 系统的进程管理的都知道,当一个进程的父进程死亡之后,它就变成了孤儿进程,会由进程号 1 的 init 进程收养,并且在它死亡时由 init 来收尸。但是,自从使用 systemd 来管理用户级服务进程之后,我发现 systemd --user 管理的进程总是在它之下,即使进程已经 fork 了好几次。systemd 是怎么做到的呢?

对一个软件的实现有不懂的想了解当然是读它的源码了。这种东西可没有另外的文档,因为源码本身即文档。当然之前我也 Google 过,没有得到结果。在又一个全新的源码树里寻寻觅觅一两天之后,终于找到了这个:

        if (arg_running_as == SYSTEMD_USER) {
                /* Become reaper of our children */
                if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
                        log_warning("Failed to make us a subreaper: %m");
                        if (errno == EINVAL)
                                log_info("Perhaps the kernel version is too old (< 3.4?)");
                }
        }

原来是通过prctl系统调用实现的。于是去翻 prctl 的 man 手册,得知PR_SET_CHILD_SUBREAPER是 Linux 3.4 加入的新特性。把它设置为非零值,当前进程就会变成 subreaper,会像 1 号进程那样收养孤儿进程了。

当然用 C 写不好玩,于是先用 python-cffi 玩了会儿,最后还是写了个 Python 模块,也是抓住机会练习一下 C 啦。有个 python-prctl 模块,但是它没有包含这个调用。

#include<sys/prctl.h>
#include<Python.h>

static PyObject* subreap(PyObject *self, PyObject *args){
  PyObject* pyreaping;
  int reaping;
  int result;

  if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &pyreaping))
    return NULL;
  reaping = pyreaping == Py_True;

  Py_BEGIN_ALLOW_THREADS
  result = prctl(PR_SET_CHILD_SUBREAPER, reaping);
  Py_END_ALLOW_THREADS

  if(result != 0){
    return PyErr_SetFromErrno(PyExc_OSError);
  }else{
    Py_RETURN_NONE;
  }
}

static PyMethodDef mysysutil_methods[] = {
  {"subreap", subreap, METH_VARARGS},
  {NULL, NULL}    /* Sentinel */
};

static PyModuleDef mysysutil = {
  PyModuleDef_HEAD_INIT,
  "mysysutil",
  "My system utils",
  -1,
  mysysutil_methods,
  NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_mysysutil(void){
  PyObject* m;

  m = PyModule_Create(&mysysutil);
  if(m == NULL)
    return NULL;
  return m;
}

编译之后,

>>> import mysysutil
>>> mysysutil.subreap(True)

然后开子进程,不管它 fork 多少次,都依然会在这个 Python 进程之下啦。

但是,这样子不太好玩呢。如果我登陆之后所有启动的子进程都在一个进程之下不是更有意思么?于是我打上了 Awesome 的主意,因为它支持运行任意的 Lua 代码嘛。于是我又给这个 prctl 调用弄了个 Lua 绑定。最终的版本如下:

#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>

#include<sys/prctl.h>
#include<sys/wait.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

static int l_setsubreap(lua_State * L){
  int reap;
  if(lua_isboolean(L, 1)){
    reap = lua_toboolean(L, 1);
  }else{
    return luaL_argerror(L, 1, "not a boolean");
  }
  if(prctl(PR_SET_CHILD_SUBREAPER, reap) != 0){
    return luaL_error(L, "prctl failed: %s", strerror(errno));
  }
  return 0;
}

static int l_ignore_SIGCHLD(lua_State * L){
  signal(SIGCHLD, SIG_IGN);
  return 0;
}

static int l_reap(lua_State * L){
  int pid, st;
  pid = waitpid(-1, &st, WNOHANG);
  lua_pushinteger(L, st);
  lua_pushinteger(L, pid);
  return 2;
}

static const struct luaL_Reg l_lib[] = {
  {"setsubreap", l_setsubreap},
  {"reap", l_reap},
  {"ignore_SIGCHLD", l_ignore_SIGCHLD},
  {NULL, NULL}
};

int luaopen_clua(lua_State * L){
  lua_newtable(L);
  luaL_setfuncs(L, l_lib, 0);
  return 1;
}

除了调用 prctl 外,还增加了显式忽略 SIGCHLD 信号,以及非阻塞地调用 waitpid 收割单个僵尸进程的函数,因为 Awesome 本身没处理子进程退出,我一不小心弄出了好几个僵尸进程……对了,那个 waitpid 要注意给弄成非阻塞的,不然一不小心就会出问题

用的时候就是这样子,可以写到rc.lua里,也可以在 awesome-client 里调用:

package.cpath = package.cpath .. ';/home/lilydjwg/scripts/lua/cmod/?.so'
clua = require('clua')
clua.setsubreap(true)
clua.ignore_SIGCHLD()

最终,我的进程树成了这样子:

htop-awesome-tree

可以看到,由 Awesome 启动的进程已经全部待在 Awesome 进程树之下了。systemd --user 是由 PAM 启动的,所以不在 Awesome 树下。但是,那些 dbus 的东西和 gconfd-2、at-spi 之类的是怎么回事呀……

7
9
2013
4

把标准输出伪装成终端

fcitx-diagnose 是 fcitx 输入法的非常优秀的诊断脚本。当输出到终端时,fcitx-diagnose 会给输出加上易于区分不同类型的消息的彩色高亮。可是,当用户把输出重定向到文件以便让其他人帮助查看时,这些高亮就没了。fcitx-diagnose 的输出很长,但如果通过管道给 less 查看的看,这些彩色也会消失。

要是 fcitx-diagnose 支持--color=always这样的选项就好了。可是 yyc 说他懒得写。getopt我只在 C 里用过,好麻烦的,所以我也懒得写。于是,我还是用我的 ptyless 好了。后来又想到,用于改变 I/O 缓冲方式的 unbuffer 和 stdbuf 应该也可以。测试结果表明,只有 unbuffer 可行,因为它是和 ptyless 一样使用伪终端的。stdbuf 则是使用 LD_PRELOAD 载入一个动态链接库的方式来设置缓冲区。

不过,既然 stdbuf 用 LD_PRELOAD 来设置缓冲区,我何不来用相同的办法改变isatty()函数的返回值呢?同时,我也学学 stdbuf,试了下__attribute__ ((constructor))指令。

#include<stdarg.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<dlfcn.h>

static int (*orig_isatty)(int) = 0;

int isatty(int fd){
  if(fd == 1){
    return 1;
  }
  return orig_isatty(fd);
}

void die(char *fmt, ...) {
  va_list args;
  va_start(args, fmt);
  vfprintf(stderr, fmt, args);
  va_end(args);
  fprintf(stderr, "\n");
  fflush(stderr);
  exit(-1);
}

__attribute__ ((constructor)) static void setup(void) {
  void *libhdl;
  char *dlerr;

  if (!(libhdl=dlopen("libc.so.6", RTLD_LAZY)))
    die("Failed to patch library calls: %s", dlerror());

  orig_isatty = dlsym(libhdl, "isatty");
  if ((dlerr=dlerror()) != NULL)
    die("Failed to patch isatty() library call: %s", dlerr);
}

然后,像 stdbuf、proxychains 那样做了个包装,不用自己手动设置 LD_PRELOAD 环境变量了。这也是我第一次使用 CMake,比 GNU 的 autotools 那套简单多了 :-)

使用方法很简单:

  1. 克隆或者下载源码
  2. 编译之
    $ mkdir -p build && cd build
    $ cmake .. # 或者安装到 /usr 下: cmake .. -DCMAKE_INSTALL_PREFIX=/usr
    $ make
    
  3. 安装之
    $ sudo make install
    $ sudo ldconfig
    
  4. 可以使用了:
    $ stdoutisatty fcitx-diagnose | less
    
Category: Linux | Tags: C代码 linux 终端 shell
6
6
2012
12

编程获取本机IPv4及IPv6地址

首先,我要通过编程直接获取,而不是去读诸如ifconfig等命令的输出。

其实是只想获取IPv6地址的,不过我猜想它们差不多,也确实看到不少相关搜索结果,于是顺带着看了。

首先,使用gethostbyname查自己通常是不行的,因为可能得到127.0.0.1,而且我猜,这样不能处理拥有多个IPv4地址的情况。另外一种方式是连上某个主机,然后调用getsockname。这样需要能够直接连上那个主机,好处是如果有多个网络接口,这样可以知道到底走的是哪个接口,调试网络时不错。我最满意的方案在这里,使用ioctl来获取。这个方法可以获取指定网络接口的IPv4地址。至于有哪些网络接口嘛,直接读/proc/net/dev吧。

import fcntl
import socket
import struct
ifname = b'eth0'
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 0x8915 是 SIOCGIFADDR
ip = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24])
print(ip)

然而,这样只能获取IPv4地址。创建个AF_INET6的 socket 传过去会报错「Inappropriate ioctl for device」。那怎么办呢?Google 没找到,我去搜了下内核源码。inet_ioctl里有对SIOCGIFADDR的处理。但是,inet6_ioctl里却没有了。

于是,我只好去下载ifconfig所属的 net-tools 的源码,找到相关代码:

#if HAVE_AFINET6
    /* FIXME: should be integrated into interface.c.   */

    if ((f = fopen(_PATH_PROCNET_IFINET6, "r")) != NULL) {
    while (fscanf(f, "%4s%4s%4s%4s%4s%4s%4s%4s %08x %02x %02x %02x %20s\n",
              addr6p[0], addr6p[1], addr6p[2], addr6p[3],
              addr6p[4], addr6p[5], addr6p[6], addr6p[7],
          &if_idx, &plen, &scope, &dad_status, devname) != EOF) {
        if (!strcmp(devname, ptr->name)) {
        sprintf(addr6, "%s:%s:%s:%s:%s:%s:%s:%s",
            addr6p[0], addr6p[1], addr6p[2], addr6p[3],
            addr6p[4], addr6p[5], addr6p[6], addr6p[7]);

这里就是ifconfig输出IPv6部分的代码了。可以看到它打开了一个奇怪的文件。跟过去,发现是

#define _PATH_PROCNET_IFINET6       "/proc/net/if_inet6"

囧,这个文件我早就发现过了的。看来和IPv4的情况不同,IPv6地址只能通过/proc里的文件获取了。而且输出成人可读格式不容易(ifconfig是自己实现的)。

PS: 我还发现了件好玩的事,在 Linux 源码的include/linux/sockios.h中,SIOCGIFINDEX中的字母 C 写漏了。通过git blame我发现,这个拼写错误在至少七年前 Linux 内核代码迁移到 git 前就修正了。Linus Torvalds 说之前的代码导入到 git 后有 3.2GB。我不得不承认这是个无比正确的决定,因为现在的.git已经有600多兆了,git 不支持断点续传,clone 下来已经很不容易了。

另外,我还联想到了 Unix 系统调用中的creat,以及 HTTP 协议中的referer :D

#define SIOCGIFINDEX    0x8933      /* name -> if_index mapping */
#define SIOGIFINDEX SIOCGIFINDEX    /* misprint compatibility :-)   */
Category: Linux | Tags: C代码 linux python 网络
10
23
2011
3

使用 fontconfig 进行字体查询

Vayn想知道如何判断一个字体是否支持中文,我也对字体的种种特性好奇,于是我再一次淹没在文档之中。先是翻了半天Pango的文档,各种字体相关的函数,还找到个pango_has_char函数。不过我没能弄明白怎么它需要的参数类型PangoFcFont怎么弄。后来查到这个函数需要底层支持,于是我直接找到 fontconfig 去了。

fontconfig 的文档不怎么样,虽然后来发现它提供了manpages、PDF、HTML、TXT等格式,但我依旧没能从文档中弄明白如何得到一个字体的信息。看了 fc-query 的源代码才知道,原来FcPattern既用来作输入,也用来作输出。查询字体时它是查询条件,而返回时它就是字体信息。

/* ===================================================================== *
 *  判断某个字符是否存在于指定的字体(文件)中
 * ===================================================================== */
#include<stdio.h>
#include<string.h>
#include<fontconfig/fontconfig.h>
/* --------------------------------------------------------------------- */
int main(int argc, char **argv){
  int ret = 0;
  FcChar8* file = (FcChar8*)"/home/lilydjwg/.fonts/迷你简启体.ttf";
  FcPattern* pat;
  FcCharSet* cs;
  FcChar32 ch;
  int count;

  FcBlanks* blanks = FcConfigGetBlanks(NULL);
  pat = FcFreeTypeQuery((FcChar8 *)file, 0, blanks, &count);

  if(FcPatternGetCharSet(pat, FC_CHARSET, 0, &cs) != FcResultMatch){
    fprintf(stderr, "no match\n");
    ret = -1;
    goto cleanup;
  }

  FcUtf8ToUcs4((FcChar8*)"简", &ch, 3);
  if(FcCharSetHasChar(cs, ch)){
    puts("Yes");
  }else{
    puts("No");
  }

cleanup:
  FcPatternDestroy(pat);
  return ret;
}
/* ===================================================================== *
 * vim modeline                                                          *
 * vim:se fdm=expr foldexpr=getline(v\:lnum)=~'^\\S.*{'?'>1'\:1:         *
 * ===================================================================== */
/* ===================================================================== *
 *  判断某个字符是否存在于指定的字体(条件匹配)中
 * ===================================================================== */
#include<stdio.h>
#include<fontconfig/fontconfig.h>
/* --------------------------------------------------------------------- */
int main(int argc, char **argv){
  FcFontSet* fs = NULL;
  FcPattern* pat = NULL;
  FcObjectSet* os = NULL;

  FcChar8* strpat = (FcChar8*)":lang=zh";
  pat = FcNameParse(strpat);
  os = FcObjectSetBuild(FC_FAMILY, FC_CHARSET, FC_FILE, (char *)0);
  fs = FcFontList(0, pat, os);
  if(os)
    FcObjectSetDestroy(os);
  os = NULL;

  FcPatternDestroy(pat);
  pat = NULL;

  if(!fs || fs->nfont <= 0)
    goto nofont;

  FcChar8 *family;
  FcChar8 *file;
  FcCharSet* cs;
  FcChar32 ch;
  FcUtf8ToUcs4((FcChar8*)"这", &ch, 3);
  int i;
  for(i=0; i<fs->nfont; i++){
    if(FcPatternGetCharSet(fs->fonts[i], FC_CHARSET, 0, &cs) != FcResultMatch){
      fprintf(stderr, "no match\n");
      FcPatternPrint(fs->fonts[i]);
      goto nofont;
    }
    if(FcPatternGetString(fs->fonts[i], FC_FAMILY, 1, &family) != FcResultMatch)
      if(FcPatternGetString(fs->fonts[i], FC_FAMILY, 0, &family) != FcResultMatch)
	goto nofont;
    printf("[%d] %s ", i, (char *)family);
    if(FcPatternGetString(fs->fonts[i], FC_FILE, 0, &file) != FcResultMatch)
      goto nofont;
    printf("(%s): ", (char *)file);
    if(FcCharSetHasChar(cs, ch)){
      puts("Yes");
    }else{
      puts("No");
    }
  }

  FcFontSetDestroy(fs);

  return 0;

nofont:
  return 1;
}
/* ===================================================================== */
Category: Linux | Tags: fontconfig C代码
9
21
2011
2

使用Xtest模拟鼠标点击

最近做一个小工具,需要模拟鼠标点击事件。当然,我可不想去调用 xdotool 或者 xmacro,效率什么不说,光是添加这么个罕见的依赖就不喜欢。顺便也好练习下 C 编程。

Xtest 的函数名长参数列表也长,不过用起来很简单。我所需要调用的函数就两个:

  • XTestFakeMotionEvent:把鼠标光标移动到指定坐标;
  • XTestFakeButtonEvent:模拟鼠标键

Xtest 的函数手册都在一个 manpage 里。看一下就知道用法了。

XTestFakeMotionEvent有五个参数,第一个是Display指针,然后依次是屏幕号、坐标和延时。屏幕号写-1就是默认了。延时我用0就好了。XTestFakeButtonEvent有四个参数,第一个依旧是Display指针,然后是按键号、是不是按下(还是放开按键)、延时。左键是1其它依次递加。不知道为什么这些函数要有个延时的参数。

#include<X11/Xlib.h>
#include<X11/extensions/XTest.h>

/* ... */

int clickAt(int x, int y){
  Display *dpy = XOpenDisplay(NULL);
  if(dpy == NULL){
    return 0;
  }

  XEvent event;

  /* get info about current pointer position */
  XQueryPointer(dpy, RootWindow(dpy, DefaultScreen(dpy)),
      &event.xbutton.root, &event.xbutton.window,
      &event.xbutton.x_root, &event.xbutton.y_root,
      &event.xbutton.x, &event.xbutton.y,
      &event.xbutton.state);

  XTestFakeMotionEvent(dpy, -1, x, y, 0);
  XTestFakeButtonEvent(dpy, 1, 1, 0);
  XTestFakeButtonEvent(dpy, 1, 0, 0);
  /* place the mouse where it was */
  XTestFakeMotionEvent(dpy, -1, event.xbutton.x, event.xbutton.y, 0);
  XCloseDisplay(dpy);
  return 1;
}

这个函数实现了点击指定的屏幕坐标,完事之后再把鼠标光标移回去。最开始是没有移回去的,然后测试的时候我经常找不到鼠标光标了。。。。

C 语言用起来挺不爽的,所以后来做了个 Python 模块。不过功能很不完整,以后有需要时再慢慢加啦。要是谁有兴趣也可以自己加了后给我发 pull request 就更好了。代码地址:https://github.com/lilydjwg/winterpy/blob/master/pyso/X.c,编译命令:

gcc -O2 -shared -lX11 -lXtst `pkg-config --cflags --libs python3` X.c -o X.so

编译后import X,然后help(X)就知道用法了。

Category: Linux | Tags: python C代码 X Window
7
31
2011
5

在 ncurses 中使用 readline

有一天,我发现了一个很好用的 Python shell——bpython。它使用了 ncurses 来做界面,使用了 pygments 来高亮代码,怎么看都比 ipython 漂亮,更不用说 Python 自己的了。不过既然它使用了 ncurses,麻烦也就来了——ncurses 不支持 readline!虽然有些模拟,但终究是不好用,M-f M-b不起使用,M-数字也不能用。于是我再次去 google 同时使用 ncurses 和 readline 这两个库的办法。

功夫不负有心人,这次终于 google 到了点有用的东西

The basic idea is to use call rl_callback_read_char() when input from the user is available (determined with select, or similar), then print 'rl_line_buffer' as you would any other string in ncurses, and optionally set A_REVERSE on the position indicated by rl_point. (or just reposition the cursor I guess, either works...)

不过可惜的是,这封邮件给出的代码在我这里并没有跑起来。其实跑起来了也用处不大,因为我需要的那部分代码独立性太差了,还是得重写。

花了一个下午,我终于弄出了一个雏形。

首先,这个rl_callback_read_char()是这么用的(文档):

#include<stdio.h>
#include<readline/readline.h>

int main(void){
  int cont = 1;

  void callback(char *text){
    if(text == NULL){
      rl_callback_handler_remove();
      putchar('\n');
      cont = 0;
    }else{
      printf("%s.\n", text);
    }
  }

  rl_callback_handler_install(">> ", callback);
  while(cont){
    rl_callback_read_char();
  }
  return 0;
}
      

首先安装个回调函数,它将在 readline 读取到一行内容时调用。当标准输入可用的时候,调用rl_callback_read_char()来读取字符。另外注意,这里我用了 gcc 的嵌套函数支持,免得弄出不少全局变量。

知道怎么用rl_callback_read_char()之后,就可以按那封邮件所说的,把它和 ncurses 联合起来了。代码修改自NCURSES Programming HOWTO。思路很简单,readline 负责读取并处理用户输入,显示是自己处理的。不过作为中文用户,纠结了半天的中文问题。最开始是有了 ncurses 之后,中文显示异常。这个是通过setlocale解决的。然后又是光标放的位置不对。于是又用上了我同样不熟悉的 wchar,自己计算光标应该放在哪里。

#define _XOPEN_SOURCE 700       /* for wcswidth and 700 is for mbsnrtowcs */
#include<wchar.h>
#include<ncurses.h>		/* ncurses.h includes stdio.h */
#include<stdlib.h>
#include<string.h>
#include<readline/readline.h>
#include<locale.h>
 
int mygetstr(char *str, int y, int x){
  WINDOW *win;
  int size, col;
  int ok = 0;
  int width;
  wchar_t wstr[80];
  char *p;
 
  getmaxyx(stdscr, size, col);
 
  void getaline(char *s){
    str = s;
    rl_callback_handler_remove();
    ok = 1;
  }
 
  rl_callback_handler_install("", getaline);
  win = newwin(1, col-x, y, x);
  while(1){
    rl_callback_read_char();
    if(ok)
      break;
    werase(win);
    strncpy(str, rl_line_buffer, 80);
    p = str;
    /* how many column chars before cursor occupies? */
    size = mbsnrtowcs(wstr, (const char**)&p, rl_point, 80, NULL);
    width = wcswidth(wstr, size);
    mvwprintw(win, 0, 0, "%s", str);
    /* put the cursor at right column */
    wmove(win, 0, width);
    wrefresh(win);
  }
  delwin(win);
  return 0;
}
 
int main(){
  char mesg[] = "Enter a string: ";
  char str[80];
  int row, col;
 
  setlocale(LC_ALL, "");        /* make ncurses handle Chinese correctly */
  initscr();
  getmaxyx(stdscr, row, col);
  mvprintw(row / 2, (col - strlen(mesg)) / 2, "%s", mesg);
  refresh();
  mygetstr(str, row / 2, (col + strlen(mesg)) / 2);
  mvprintw(LINES - 2, 0, "You Entered: %s", str);
  getch();
  endwin();
 
  return 0;
}
      

注意:此代码只是演示用,缓冲区溢出什么的我都没处理。

终于搞定了 C 下结合两者的使用,接下来 Python 版思路是有了,但因为其标准库 readline 中没有提供rl_callback_read_char()函数,所以只能用 ctypes 了。下面是在 Python 里使用rl_callback_read_char()的示例,ncurses 部分我暂时不想折腾了。

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

import sys
import readline
import ctypes
import ctypes.util

rllib_path = ctypes.util.find_library('readline')
rllib = ctypes.CDLL(rllib_path)

def callback(s):
  if s is None:
    rllib.rl_callback_handler_remove()
    sys.stdout.write('\n')
    sys.exit()
  elif not s:
    pass
  else:
    print('%s.' % s.decode())
    # 这样也可以
    # print(readline.get_line_buffer())

cbfunc = ctypes.CFUNCTYPE(None, ctypes.c_char_p)

rllib.rl_callback_handler_install.restype = None
rllib.rl_callback_handler_install(ctypes.c_char_p(b">> "), cbfunc(callback))

while True:
  rllib.rl_callback_read_char()
      

2011年8月4日更新:今天终于完成了个 quick and dirty 的 Python 版:

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

import sys
import readline
import ctypes
import ctypes.util
import curses
import struct
from 字符集 import width
 
rllib_path = ctypes.util.find_library('readline')
rllib = ctypes.CDLL(rllib_path)
 
def getstr(win, y, x):
  _, col = win.getmaxyx()
  inputbox = curses.newwin(1, col-x, y, x)
  ret = ''
  ok = False
  def callback(s):
    nonlocal ok, ret
    if s is None:
      rllib.rl_callback_handler_remove()
      raise EOFError
    elif not s:
      ok = True
    else:
      ret = s.decode()
      ok = True
  
  cbfunc = ctypes.CFUNCTYPE(None, ctypes.c_char_p)
  
  rllib.rl_callback_handler_install.restype = None
  rllib.rl_callback_handler_install(ctypes.c_char_p(b""), cbfunc(callback))
  
  while True:
    rllib.rl_callback_read_char()
    if ok:
      break
    inputbox.erase()
    # 这样获取的值不对。。。
    # bbuf = ctypes.string_at(rllib.rl_line_buffer)
    buf = readline.get_line_buffer()
    bbuf = buf.encode()
    inputbox.addstr(0, 0, buf)
    rl_point = struct.unpack('I', ctypes.string_at(rllib.rl_point, 4))[0]
    w = width(bbuf[:rl_point].decode())
    inputbox.move(0, w)
    inputbox.refresh()

  del inputbox

  return ret

msg = '输入字符串:'
win = curses.initscr()
curses.noecho()
row, col = win.getmaxyx()
win.addstr(row // 2, (col - width(msg)) // 2, msg)
win.refresh()
s = getstr(win, row // 2, (col + width(msg)) // 2)
win.addstr(row - 2, 0, '你输入了: ' + s)
win.getch()
curses.endwin()
      
Category: Linux | Tags: C代码 ncurses python readline

| Theme: Aeros 2.0 by TheBuckmaker.com