0%

【Linux技术分享】易联众扫码丢失问题分析

易联众扫码丢失问题分析

问题概述

易联众五代扫码机器,在wine下扫描医保码或者就诊单存在字符丢失情况,问题复现率与机器性能强度成反比。在现场中存在一台性能较差的赛扬cpu机器,复现问题成功率在百分之八十左右。

window下输入框正常现象如下图:

图1

nfs下wine异常现象如下图:

图2

经验证问题复现环境为:

软件环境:amd架构,nfs-5.0-U212版本

硬件环境:cpu J1900/四核心

wine版本:上游wine-8.6

输入法版本:fcitx5_5.0.5(fcitx5为开源代码并社区讨论更新较多,在分析此问题时可以进行源码对照)

问题分析

通过对上次易联众扫码大小写显示不正常的问题分析学习后,首先可以排除是硬件问题,猜测该问题大致原因可能为:

  • 猜测1:wine下存在显示问题,导致字符未能正常显示至系统中。
  • 猜测2:在系统中,按键消息首先发送至输入法中如下图,由输入法进行相应在发送至窗口,怀疑输入法接收或发送这块存在自身bug,未能将完整消息发送至窗口
    图3

猜测1

  1. 在学习和观察wine代码,发现他的事件统一处理在dlls/winex11.drv/event.c文件的process_events中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    static BOOL process_events( Display *display, Bool (*filter)(Display*, XEvent*,XPointer), ULONG_PTR arg )
    {
    XEvent event, prev_event;
    int count = 0;
    BOOL queued = FALSE;
    enum event_merge_action action = MERGE_DISCARD;

    prev_event.type = 0;
    while (XCheckIfEvent( display, &event, filter, (char *)arg ))
    {
    count++;
    if (XFilterEvent( &event, None ))
    {
    continue;
    ‘’‘’
    }
    }
    if (prev_event.type) queued |= call_event_handler( display, &prev_event );
    }
  2. 在经过消息过滤与消息合并后通过call_event_handler进行对每个消息分类处理,针对键盘按下的操作,使用X11DRV_KeyEvent进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    BOOL X11DRV_KeyEvent( HWND hwnd, XEvent *xev )
    {
    XKeyEvent *event = &xev->xkey;
    char buf[24];
    char *Str = buf;
    KeySym keysym = 0;
    WORD vkey = 0, bScan;
    DWORD dwFlags;
    int ascii_chars;
    XIC xic = X11DRV_get_ic( hwnd );
    DWORD event_time = EVENT_x11_time_to_win32_time(event->time);
    Status status = 0;

    TRACE_(key)("type %d, window %lx, state 0x%04x, keycode %u\n",
    event->type, event->window, event->state, event->keycode);

    if (event->type == KeyPress) update_user_time( event->time );

    /* Clients should pass only KeyPress events to XmbLookupString */
    if (xic && event->type == KeyPress)
    {
    ........
  3. 打印wine中的key的日志通道,对比发现wine未收到已丢失的键盘按键事件,需要继续分析此问题
    图4

猜测2:

  1. Fcitx 的基本架构是客户端/服务器架构,客户端,在 Fcitx 里面对应的词是“输入上下文(Input Context)”,当一个程序不能用输入法的时候,一般最常见的情况就是这个程序并没有采用正确的方式和输入法进行通信。目前在 Linux 下的客户端通信方式类型如下:
    图12

    • xim:在 X11 下理论上是通用的,实现由 Xlib 完成。和 Xlib 的事件处理结合非常紧密,采用了一种直接原生和 Xlib 按键转化字符串的方式来实现。但 Qt 5 和 SDL 2 由于是通过 xcb 来处理 X11 的通信,无法使用xim协议,所以已去除此协议。虽然 Xlib 现在内部已经变成了基于 xcb 的实现,但是,如果原生就是 xcb 的情况,是无法反过来再用 xlib 的。但是在 Fcitx 5 上,开发了一个纯 xcb 的 XIM 实现模块-libxcb-imdkit。
    • dbusfrontend:这个就是 Fcitx 自己的协议了,基于 dbus 实现的。需要程序配合使用对应的 DBus 接口,主要就是通过 IM Module。而这个主要就是通过常见的两板斧:GTK_IM_MODULE 和 QT_IM_MODULE 环境变量控制,顾名思义Gtk/Qt实现的界面窗口使用此协议。
    • waylandim:在wayland下实现了 zwp_input_method_v1 和 zwp_input_method_v2 的支持。
    • fcitx4frontend 和 ibusfrontend:模拟原本 Fcitx 4 自己的 IM Module 协议和模拟 IBus 的。
  2. 通过对wine下的输入法模块学习,发现wine通过xim框架与输入法进行通讯,wine下输入法创建比较关键的地方是调用的XCreateFontSetXVaCreateNestedList

  3. 将wine下的输入法关键地方提炼出来,使用x11实现一个窗口输入框

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    void openxim()
    {
    ‘’‘’‘’
    xim = XOpenIM(display, NULL, NULL, NULL);
    if (xim == NULL) {
    fprintf(stderr, "Cannot open input method\n");
    exit(0);
    }
    /* create callbacks for the input method */
    destroy.client_data = NULL;
    destroy.callback = (XICProc)icdestroy;

    preeditstyle = XIMPreeditCallbacks;
    statusstyle = XIMStatusNothing;
    for (int i = 0; i < imstyles->count_styles; i++) {
    if (imstyles->supported_styles[i] & XIMPreeditCallbacks) {
    preeditstyle = XIMPreeditCallbacks;
    break;
    }
    }
    ‘’‘’‘’
    XFontSet fontset = XCreateFontSet(display, "fixed", &missing_charset_list, &missing_charset_count, NULL);
    }
    /* create list of values for input context */
    preedit = XVaCreateNestedList(0, XNFontSet, fontset,
    XNPreeditStartCallback, &start,
    XNPreeditDoneCallback, &done,
    XNPreeditDrawCallback, &draw,
    XNPreeditCaretCallback, &caret,
    NULL);
    if (preedit == NULL)
    errx(1, "XVaCreateNestedList: could not create nested list");
    /* create input context */
    xic = XCreateIC(xim,
    XNInputStyle, preeditstyle | statusstyle,
    XNPreeditAttributes, preedit,
    XNClientWindow, window,
    XNDestroyCallback, &destroy,
    NULL);
    if (xic == NULL)
    errx(1, "XCreateIC: could not obtain input method");
    ‘’‘’‘’‘’
    }
  4. 进行扫码测试如下图发现也是存在内容丢失的,通过本轮测试基本可以排除扫码丢失非wine下问题
    图5

  5. 开启fcitx5下全量日志通道fcitx5 -r --verbose *=5,进行扫码时,若存在消息丢失则输出大量XIM filtered event
    图6

  6. 查看fcitx5对应版本的源码,发现在调用xcb_im_filter_event函数中返回true时会打印XIM filtered event日志,xcb_im_filter_event函数为libxcb-imdkit函数实现,通过步骤1的学习,我们发现此模块功能为过滤和处理与输入法相关的 X11 事件。
    图13

  7. 查看此模块的源码,发现使用如下四个函数进行消息事件的判断处理

    1
    2
    3
    4
    5
    6
    bool xcb_im_filter_event(xcb_im_t *im, xcb_generic_event_t *event) {
    return _xcb_im_filter_xconnect_message(im, event) ||
    _xcb_im_filter_selection_request(im, event) ||
    _xcb_im_filter_client(im, event) ||
    _xcb_im_filter_destroy_window(im, event);
    }
  • _xcb_im_filter_xconnect_message:过滤并处理与 XIM 连接相关的消息。
  • _xcb_im_filter_selection_request:过滤并处理 X11 选择请求事件。
  • _xcb_im_filter_client:过滤并处理来自客户端的输入法相关事件。
  • _xcb_im_filter_destroy_window:过滤并处理窗口销毁事件。
  1. 在键盘事件丢失时,是通过_xcb_im_filter_client进行处理过滤,经过源码学习发现一个比较关键的函数调用: xcb_get_property_reply_t *reply = xcb_get_property_reply(conn, cookie, NULL);,经过日志打印其reply->type发现其值为0,经过对比在正常扫码的时候reply->type均不为0.
    图7
    图8

  2. xcb_get_property_reply函数用于从 X 服务器获取属性请求的回复,当reply->type为0时说明请求的属性不存在获取错误,但是在扫码时,按键属性是偶现丢失并非全部丢失,所以需要继续定位type为0的具体原因。

  3. 由于本地的libxcb-imdkit版本为1.0.2为较低版本,上游最新版本为1.0.8,类似这种严重问题大概率中间版本存在解决,翻阅代码的committed message,发现在1.0.3到1.0.5时存在一个提交,用于解决事件速度较快字符丢失的问题,通过阅读 committed message 发现该问题原因大致为:在 XIM 协议中,为了传递输入法相关的信息,客户端和服务器之间使用窗口属性(window property)来进行通信。这些属性的名称是轮换使用的,类似于一个循环缓冲区。Xlib 使用一个固定的轮换周期(cycle 20)。这意味着属性名称会在 20 个不同的名称之间循环使用。如果客户端发送事件太快,可能会导致属性在被读取之前被覆盖。这会导致读取属性时出现问题。需要增加一个更大的缓冲区,确保所有数据都被读取。这是为了避免由于属性名称轮换而导致的数据丢失。。
    图9

  1. 更新libxcb-imdkit后再进行测试验证,发现新版本可以,能够正常运行使用。
    图10

分析结论

通过上述分析与验证代码,可以初步猜测在nfs系统上若使用x11实现的可输入中文界面窗口进行快速输入,会偶现出现字符丢失问题。

解决方案

  • 方案1:卸载输入法,将按键消息直接通过窗管发送至应用,不走输入法则不会出现丢失问题。
  • 方案2:在fcitx5下更新libxcb-imdkit模块版本>=1.0.5,在fcitx4下则需要修复其x11模块的功能。

修改后效果如下:
图11

小结

这个问题需要对x11和输入法框架比较熟悉,理解输入法与几个基础界面UI框架通讯协议流程。由于输入法与不同框架实现的界面程序使用不同协议,会让开发者对问题判断存在疑惑,特别是在wps/终端/文本编辑器等大众可输入应用进行扫码测试时,未能出现字符丢失情况,开发者就会怀疑是wine本身问题,此时就需要阅读wine下关于x11代码,进行模块提炼完成demo测试,才会更精准理解认识问题所在。