• 首页
  • 加入
  • RSS
  • Monday, December 17, 2018

    头段时间入了一个大坑儿,大概被坑了有一个月之久,出来之后同事还不忘嘲讽一番:”这么个事情就搞了一个月,看吧,你果然是老了“。听了这句话,心里真是百般滋味,但转念一想,”我年轻的时候做事好像也不怎么快“,顿时也就释怀了 🙂

    这个坑就是”给 FreeRDP 的 RAIL 模式添加托盘支持“,当然,跟所有的需求一样,这么具有总结性而又直指根源的需求描述,绝对不是它最原始的模样——我刚接到这个坑的时候,它是这样的:FreeRDP 的 RAIL 模式下,应用的托盘在我们 DDE 下不显示。请注意这里说得是不显示,而不是后来发现的压根儿没有支持

    FreeRDP

    说到这,可能有读者还不了解 FreeRDP 和 RAIL,所以先简单介绍一下。

    RDP 其实是一个协议名称,全称 Remote Desktop Protocol(远程桌面协议),是微软公司开发的一套用于远程桌面展示和操作的协议,FreeRDP 就是它在开源世界的实现咯。而 RAIL 的全称是 Remote Application Integrated Locally (远程应用本地集成),其实就是非常类似大家熟悉的虚拟机的”无缝模式“,通过将应用的显示跟本地环境相融合,让用户完全感受不到这个应用其实不在本机运行——就是这么一种技术。

    问题也就出在这,我当时第一反应是这么老的技术实现肯定比较完整了,托盘没有显示出来应该是跟 DDE 的兼容性有点小问题,稍微修一下就完了,三下五除二的事情,所以满口答应了下来……

    经过

    既然答应了,硬着头皮也要顶下去的。何况调 BUG 这种事情——不管是不是我们自己的问题——在深度都是家常便饭。慢慢地,调各种项目的 BUG 竟然成了我的一种乐趣——每次开始接手一个新的项目的时候,我都把自己当成了福尔摩斯或者胡八一,或者也可以是其他全世界最聪明的那类人 ?,在通过代码找寻问题线索的过程中,慢慢成为这个项目世界中的主宰,解开真相……

    额……不好意思,白日梦又发作了一会儿。总之,这次也不例外,而且刚好这次在调问题的过程中有记录几个关键环节,所以打算把中间的过程写成日记性质的记录,看看能不能有更好的阅读效果:

    2018-11-14

    从”沈老板“那收到需求,说 FreeRDP 在我们系统上有问题,应用的托盘显示不出来,QQ之类的程序关闭了窗口以后就没办法显示出来了,无法使用。这丫的又拿刘老大来压我……呵呵,想削他。不过看在他快要当爸爸的份上,还是算了。问了下时间要求,大概需要两周左右有初步的结果。不过我自己最近没有什么时间,先把锅丢给了印象中还比较熟悉网络协议的 @Blumia 同学。

    2018-11-15

    从 @Blumia 那收到反馈,可能 FreeRDP 没有实现托盘图标这部分的功能,我怕他一个人搞不定,简单翻了翻 FreeRDP 的项目 wiki 和 RDP 的一些介绍,给了他,让他先帮忙找一下需要补充实现部分的代码结构。

    2018-11-16

    没时间处理。

    @Blumia 搭了测试环境。

    中间几天两人都没有时间处理 FreeRDP事情。

    2018-11-22

    留了少部分时间,看了 FreeRDP 的代码,大概找到了托盘图标相关处理应该在的位置。

    • RAIL 主要接口的实现都在 xf_rail.c 中。
    • 托盘图标相关的处理在 xf_rail_no``t``ify_icon_* 相关的函数,这些函数在 xf_rail_re``gi``s``t``er_``u``pdate_call``b``a``c``ks 里面被注册到 rdpWind``o``wUpdate 对象上。
    • 部署了一份测试服务器的虚拟机。
    • 在 wf_rail.``c 中发现一个 PrintRailI``c``onInfo 函数,放在 xf_``r``ail_notify_i``c``on_common 中打印了获取到的图标的信息,发现能正常获取一些图标的数据。
    • xf 应该是 x11 freerdp 的缩写,而 wf 应该是 windows freerdp 的缩写。 中间又是几天没有时间处理 FreeRDP 的事情。

    2018-11-27

    有半天的时间看 FreeRDP 的代码,同时跟 FreeRDP 的邮件列表发了邮件询问相关技术问题,主要是为了验证自己的想法,没有指望有回复或者什么比较大用处的信息,只是希望如果自己想法是错的,有人及时纠正一下。

    • 图标显示的问题不打算优先处理,现在的问题变成如何让服务端知道了本地用户点了托盘图标。
    • 搜了一下 event 相关的文件,发现 x``f_event.c ,怀疑 X 相关的事件都是在这里面处理的,这个也不用急着去证明,先看看 client 怎么让 server 感知本地的事件。
    • 没有头绪,只好看了一下 RAIL 的 主要协议,发现 Cli``e``nt Notify Ev``en``t
    • 怀疑托盘图标在 client 端(本地端)的事件是通过 ClientNotifyEvent 发送给 server 端的。
    • ClientNotifyEvent 相关:
      • rail_main.``c 中的 Vi``r``tualChann``e``lE``n``tryEx 应该是 RDP 中 RAIL 相关的 channel 处理的函数。

    2018-11-28

    上午继续看了 FreeRDP 的代码。

    • V``i``rtualCha``n``nelEntryEx 中给 RailClientContext 设置的哪些成员函数,有些函数(Server开头的)都是需要真正的 client 去实现的,Client 开头的函数(包括 ClientNotifyEvent)都是默认有实现,但是这些 Client 开头的函数都是在哪调用的呢? 找到重要线索:
    /**
    * The position of the X window can become out of sync with the RDP window
    * if the X window is moved locally by the window manager.  In this event
    * send an update to the RDP server informing it of the new window position
    * and size.
    */
    void xf_rail_adjust_position(xfContext* xfc, xfAppWindow* appWindow)
    {
    RAIL_WINDOW_MOVE_ORDER windowMove;
    
    if (!appWindow->is_mapped || appWindow->local_move.state != LMS_NOT_ACTIVE)
        return;
    
    /* If current window position disagrees with RDP window position, send update to RDP server */
    if (appWindow->x != appWindow->windowOffsetX ||
        appWindow->y != appWindow->windowOffsetY ||
        appWindow->width != appWindow->windowWidth ||
        appWindow->height != appWindow->windowHeight)
    {
        windowMove.windowId = appWindow->windowId;
        /*
         * Calculate new size/position for the rail window(new values for windowOffsetX/windowOffsetY/windowWidth/windowHeight) on the server
         */
        windowMove.left = appWindow->x;
        windowMove.top = appWindow->y;
        windowMove.right = windowMove.left + appWindow->width;
        windowMove.bottom = windowMove.top + appWindow->height;
        xfc->rail->ClientWindowMove(xfc->rail, &windowMove);
    }
    }
    
    • 其中有主动调用 RailClientContext 的 ClientWindowMove 函数。这个函数又是 xf_``e``v``en``t.``c 中 xf_``e``v``en``t_C``o``nfig``u``r``e``Notify 有调用,再加上这个函数的注释说明,差不多能证明所有的 X事件相关的都是在 xf_event.c 中处理的,跟之前的猜测一致。
    • 那样的话如果想发送事件到 server,应该就是在 xf_event.c 中收到我们自己创建的托盘图标的点击事件后,发送一个 ClientNofityEvent 。 尝试在收到 xf_r``a``il_``no``ti``f``y_``i``con_u``p``d``a``te 的时候主动调用一次 Cli``ent``NotifyEvent 看看会发生什么。
    RAIL_NOTIFY_EVENT_ORDER notifyEvent;
    notifyEvent.windowId = orderInfo->windowId;
    notifyEvent.notifyIconId = orderInfo->notifyIconId;
    notifyEvent.message = NIN_SELECT;
    
    xfContext* xfc = (xfContext*) context;
    xfc->rail->ClientNotifyEvent(xfc->rail, &notifyEvent);
    
    • 选择 message 为 NIN_SELECT 是因为根据 r``ail.h 里面仅有的零星注释,只能推测这个可能是针对托盘的。
    • 试了下,没有任何反应。尝试换成 NIN_KEYSELECT ,更不行。
    • 硬着头皮又翻了一下协议,发现 Notificat``i``o``n``I``c``on Info``r``mation 这段,随便翻了一下,看起来没有有用信息。
    • 偶发奇想搜了一下 select 关键字想看一下这个到底是什么意思,偶然发现 Cli``en``t Notify E``ve``nt PDU 这一节(能跟源码 r``ai``l``.h 里面的一些注释对应上),里面有 WM_LBUTTONDOWN 、 WM_LBUTTONUP 等针对托盘图标的动作定义。
    • 真是对自己做事情毛毛躁躁的行为无语了,要不是心血来潮,差点就错过这么重要的信息。
    • 把 message 改成 WM_LBUTTONDOWN 和 WM_LBUTTONUP ,满怀期待。
    • 测试还是无效果……仍旧不死心,怀疑测试程序(@Blumia 同学搞的一个音乐程序)的稳健程度。
    • 使用 TIM 再试,还是不行,不能唤出主窗口。
    • 感觉走进了死胡同。

    2018-11-29

    继续看 FreeRDP 的问题,主窗口隐藏后不能显示的问题太奇怪了,得找一个简单点的程序,排除复杂影响。

    • 让 @zccrs 写了一个简单的窗口程序,定时隐藏、显示窗口。
    • 发现程序窗口隐藏后无法再显示出来……
    • 赶紧给上游报了一个 issue ,希望上游能修复。但是也不能期望上游很快能修复这个问题,所以自己还是尝试看代码……
    • 上游回复还挺快的,但是对方好像是 FreeRDP 目前的维护者,说自己对 RAIL 这部分协议本身还不是特别熟悉。
    • 不能依赖的上游不是好上游,继续看代码,发现在窗口的显示隐藏主要是通过 WINDOW_STATE_ORDER 中的 s``h``o``wState 控制的,处理的函数是 xf_``r``ail_window_``c``o``m``mon ,里面调用了 xf_ShowWin``d``ow 这个函数,但是这个函数在该显示窗口的时候只是调整了一下窗口的最大化、最小化状态,并没有 Map 这个本地窗口。
    • 加了 XMapW``in``dow(xf``c``->di``s``pl``a``y, appWi``n``dow->ha``n``dl``e``) 这行,满心期待 bugfix。
    • 编译代码测试,发现窗口连关闭都不能关闭了……
    • xfContext 的 appWindow 是本地窗口的一个抽象表示。

    2018-12-03

    觉得这个事情没有什么太大的希望了,不过既然已经知道托盘图标的显示方式和事件的发送,但是没有实际实现,到时候”沈老板“来问,也不好说都是在脑子里,干脆先把之前测通但是没有实现的内容实现一下。

    • 托盘窗口加好了,事件也都加上了。
    • 眼看着都快要完美了,就差那么一点问题没有解决,实在是不甘心,继续死磕那个问题。
    • 尝试了各种手段调试,跟整个程序的命令传递,都没有能解决问题。

    2018-12-04

    调试了一天,一遍又一遍看窗口事件,一点一点排除事件处理函数,终于发现了上游犯的一个低级错误,我很怀疑当时作者有没有测试一下 🙁

    做了修复,提交了 PR,并且顺利合并。

    心情终于舒畅了。

    中间有事请假一天

    2018-12-06

    托盘图标也画上了,不过怎么感觉颜色有点偏。

    调了一下颜色的格式(RGBA -> BGRA),图标显示正常了。

    事情终于告一段路了。

    结束

    折腾了这么长时间,事情终于搞定了,这应该是最近一年里面时间拉的最长的 BUG 了。

    实现算是完了,也能使用。但是还有一些细节没有特别完善,已提交提交到上游 一个新PR ,希望能早日合并造福一方用户。

    感想

    感觉我之前对 wine 有偏见,一直比较拒绝使用(或者大量使用)wine 的东西,但是实际上在修复 FreeRDP 的过程中,我竟然觉得这也是一种不错的解决方案……仔细想想,还是 wine 方便一点,至少不需要依赖一个服务端。

    准备入坑 wine 啦 ~(≧▽≦)/~

    Wednesday, May 4, 2016

    • “五仁”官方名称:zScript
    • 变量使用关键字"var"进行声明,变量本身无类型
    • 变量声明可放在源码中任何位置,可在任何位置被调用,例如,var a在源码中最后一行,也可以在第一行使用变量a
    • 每条语句的结尾必须是';‘或换行符(后面准备改成强制使用’;‘结尾)
    • 目前支持的数据类型:int、double、bool、string、list、tuple、object 、undefined
    • 函数也是object类型
    • string的定义为 '’ 或 "" 中所包裹的内容,支持C和JavaScript风格的字符转义
    • 支持的运算符: =、+、-、、/、%、&、|、~、^、&&、||、+=、-=、=、/=、%=、&=、|=、~=、^=、&&=、||=、!、>、<、=、==、===、!=、!==、++、–
    • 支持匿名函数,函数闭包,多值返回
    • 支持的分支结构:if else
    • 支持的循环结构:goto while for(后面准备加入do while、foreach)
    • while for 循环结构中使用break跳出循环,continue立马开始执行下一次循环,break和continue可组合使用 例如:break, break 则可以直接跳出两层循环,组合使用时continue只能放在末尾
    • 支持switch case语句,可枚举int bool string undefined

    Flex(Lexical Analyzar)

    • Lex是一个产生词法分析器的工具(最早是Eric Emerson Schmidt和Mike Lesk制作)是许多UNIX系统的标准词法分析器(lexical analyzer)产生程序,而且这个工具所作的行为被详列为POSIX标准的一部分。而Flex就是由Vern Paxon实现的Lex
    • FLex读进一个代表词法分析器规则的输入字符串流,然后输出C/C++语言的词法分析器源代码。
    • wiki:https://en.wikipedia.org/wiki/Flex_%28lexical_analyser_generator%29

    Flex的文件结构

    • 文件分成三个区块,均以一个只有两个百分比符号(%%)的单行来分隔,如下:

    定义区域 %% 规则区域 %% C/C++代码区

    • 定义区块是用来定义宏以及导入C写成的头文件所在区块。在这里面也可以写一些C代码,这一些代码会被复制到产生出来的C源代码的开头部分。
    • 规则区块是最重要的区块;这里将样式与C的陈述(statement)串连在一起。这一些样式都是正则表达式。当lexer看到输入里面有合乎给定的样式时,则会操作相对应的C代码。这就是flex运作的基础。
    • C代码区块的内容会原封不动的照搬到产生出来的C源代码里面。

    小例子

    %{
     #include 
    %}
    number [0-9]+
    %option noyywrap
    %% // 第一部分结束
    {number} {
        printf("number: %s\n", yytext);
    }
    %% // 第二部分结束
    int main()
    {
        yylex();
        return 0;
    }
    
    • %option noyywrap用于指明未定义yywrap函数,此函数用于在文件(或输入)的末尾调用。如果函数的返回值是1,就停止解析。否则等待输入后继续解析 注:正则表达式是使用单个字符串来描述、匹配一系列字符串的规则

    wiki:https://zh.wikipedia.org/wiki/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F

    Bison

    Bison的文件结构

    • 和Flex一样,也分为三个部分
    • 分别是声明、语法规则和代码段,使用%%分开

    定义区域 %% 规则区域 %% C/C++代码区

    • 第一部分和Flex一样,用于存放终结符的声明、符号优先级和符号类型等定义
    • 第二部分用来定义文法规则
    • 第三部分也是用来存放C/C++代码

    来个小例子

    %{
    #include 
    #define YYSTYPE int
    
    void yyerror(char const *);
    int yylex(void);
    %}
    
    %token NUMBER
    
    %debug
    
    %%
    
    start:  { printf("yylval = %d\n", yylval);}
        | NUMBER {
            printf("NUMBER %d\n", yylval);
        }
        start NUMBER {
            printf("start NUMBER %d\n", yylval);
        }
        ;
    
    %%
    
    void yyerror(char const *msg)
    {
        printf("error: %s\n", msg);
    }
    
    int yylex()
    {
        printf("&gt;&gt;&gt;");
    
        char str[10] = {};
    
    if(scanf("%s\n", str) == EOF)
        return 0;
    
    int number = atoi(str);
    
    printf("%d\n", number);
    
    yylval = number;
    
    return NUMBER;
    }
    
    int main()
    {
    return yyparse();
    }
    
    • %token用于定义一个终结符
    • %debug指定bison在生成的代码中加入debug输出(和bison的-t参数一样)
    • yyerror在分析过程中有语法错误时被调用
    • yylex用于给分析器返回token(在生成的代码中自动被调用) 注:终结符是指文法规则中不能再被分解的最小单位,非终结符则是相反的概念。

    例如上面的文法规则中“start”就是非终结符,NUMBER就是一个终结符。

    wiki:https://zh.wikipedia.org/wiki/%E7%B5%82%E7%B5%90%E7%AC%A6%E8%88%87%E9%9D%9E%E7%BB%88%E7%B5%90%E7%AC%A6#

    语法介绍

    变量的定义

    变量使用关键字“var”进行定义。

    zScript提供了两种形式的变量定义语句。

    1.直接定义的形式,该语句的一般形式为:

    var 标识符;
    

    该语句的含义是:定义变量名为“标识符”的变量。例如:

    var a;
    

    该语句的作用是:定义一个名为“a”的变量。 2.定义同时给变量初始化赋值,该语句的一般形式为:

    var 标识符 = 表达式;
    

    该语句的含义是:定义变量名为“标识符”的变量,且给变量赋值为表达式的结果。例如:

    var a = 1 + 2;
    

    该语句的作用是:定义一个名为“a”的变量,且赋值为3。 “var”关键字可同时定义多个变量,每个变量之间使用符号“,”分割。

    匿名函数的定义

    匿名函数定义的一般形式为:

    (形参列表) {
        语句组
    }
    

    该语句的含义是:生成一个函数对象,例如:

    (a) {
        console.log(a);
    }
    

    该语句的作用是:定义一个函数,它有一个名为“a”的形参,执行此函数会将a的值打印到屏幕上。 形参列表,可为空、一个、多个,多个形参之间使用“,”分割。

    数组的定义

    数组定义的一般形式为:

    var a = [表达式1, 表达式2, ...];
    

    该语句的含义是:生成一个数组,数组元素为中括号中表达式的结果。例如:

    var a = [1, 2, 3];
    

    该语句的作用是:定义一个包含三个元素的数组,元素的值分别为1、2、3,然后赋值给变量a; 数组使用“[”“]”定义,中间内容可以为空、一个表达式、多个表达式,多个表达式之间使用“,”隔开。

    a = a[0];
    

    取数组元素语法和C语言一样,都是中括号中写入数组下标。

    选择控制语句

    一个选择结构,包括一组或若干组操作,每组操作称为一个分支。通过选择控制语句可以实现选择结构。选择控制语句包括if语句、switch语句及起辅助控制作用的break语句。

    If语句用于计算给定的表达式,根据表达式的值是否为假,决定是否执行某一组操作。

    Switch语句首先求解一个表达式,然后根据计算结果的值,从哈希表中查询该从哪一组操作开始执行。

    Break语句用于switch结构中,用于终止当前switch结构的执行。

    if语句

    zScript提供了两种形式的if语句。

    1.单if子句的if语句。该if语句的一般形式为:

    if(表达式)
    {
        语句组
    }
    

    该语句的含义是:只有表达式的值为非零值时,才执行其内部的语句组。例如:

    if ( a &gt; b )
    {
        console.log(“hello”);
    }
    

    该语句的作用是:当a的值大于b的值时(此时,“a>b”的值为真,为非假值),在屏幕上显示“hello”;否则,不显示“hello”。 2.带else子句的if语句。该if语句的一般形式为:

    if ( 表达式 )
    {
        语句组1
    } else {
        语句组2
    }
    

    该语句的含义是:当表达式的值为非假时,执行语句组1,而不执行语句组2;否则,即表达式的值为假时,执行语句组2,而不执行语句组1。例如:

    if ( a &gt; b )
    {
        console.log(“hello1”);
    } else {
        console.log(“hello2”);
    }
    

    该语句的作用是:若a的值大于b的值(此时“a>b”为真,为非假值),则在屏幕上显示“hello1”,而不显示“hello2”;否则,即表达式的值为假时,显示“hello2”,而不显示“hello1”。

    switch语句

    Switch语句与if语句一样,也可以实现分支选择。但if语句是判断一个表达式的值是否为假,决定是否执行某个分支;而switch语句是计算一个表达式的值,根据计算结果,从哈希表查询从哪个分支开始执行代码。Switch语句的一般形式为:

    switch( 表达式 )
    {
        case 常量1:
        语句组1
        case 常量2:
        语句组2
        ...
        case 常量n:
        语句组n
        default:
        语句组 n + 1
    }
    

    switch语句的执行过程:

    • 1.求解“表达式”的值;

    • 2.如果“表达式”的值与某个case后面的“常量”的值相同,则从这里开始顺序执行语句。结果switch执行有两种形式:一是遇到break语句为止;二是未遇到break语句,则程序依次执行完所有语句组。

    • 3.如果“表达式”的值与任何一个case后面的“常量”的值都不相同,当有default子句时,则执行default后面的语句,如果没有default子句,则结束switch。

    其中break的一般形式为

    break;
    

    循环控制语句

    while语句

    while语句的一般形式为

    while( 表达式 )
    {
        循环体
    }
    

    while语句的执行过程:

    • 1.求解小括号中表达式的值。如果表达式的值为真,转第2步;否则转第3步。

    • 2.执行循环体,然后转第1步。

    • 3.执行while语句后面的语句。

    小括号中表达式的值是否为假,决定着循环体是终止还是继续循环。因此,该表达式的值为循环条件。while循环语句的执行特点是,先判断循环条件是否成立,然后决定是否执行循环体。

    当while语句的循环体只包含一条语句时,包含该循环体的“{}”可以省略。

    for语句

    在两种循环语句中,for语句最为灵活。for语句的一般形式为:

    for (表达式1; 表达式2; 表达式3)
    {
        循环体
    }
    

    for语句的执行过程:

    • 1.求解表达式1的值。

    • 2.求解表达式2的值,若其值为假,则结束循环,转第4步;若其值为真,则执行循环体。

    • 3.求解表达式3。

    • 4.结束循环。

    对象的定义

    对象定义的一般形式为:

    {
        属性名1: 属性的值,
        属性名2: 属性的值,
        ...
        属性名n: 属性的值,
        属性名n+1: 属性的值
    }
    

    例如:

    var object = {
        name: "张三",
        age: 18
    };
    

    变量object即是一个对象,它包含两个属性。 注:当对一个对象不存在的属性赋值时会将此属性加入到对象中,例如:

    object.sex = '男';
    

    对象object中就会多出一个“sex”属性。

    推荐