• HOME
  • JOIN
  • RSS
  • Saturday, December 25, 2021

    DDE 现在正在做 Wayland 的支持,所以我们需要对目前的桌面环境结构进行调整, 考虑到 GNOME 和 KDE 都已经使用 systemd 来管理 session,我认为 deepin 团队也可以考虑这一步了。

    为什么需要 systemd?

    目前 systemd 作为事实上胜利的 init 进程,它现在负责的功能已经越来越多了,支持但不限于:udevsleep/hibernate/suspenddbusservices 等。现在越来越多的程序也开始使用 systemd 管理后台服务。

    不使用 systemd 来管理 session 会有什么问题吗?

    存在一个进程逃逸问题,不过这个问题与是否采用 systemd 管理 session 没有太大关系。

    我先来简单介绍一下目前 DDE 的工作模型:

    首先,DDE 使用 LightDM 作为显示服务器(Display Manager),DDE 提供了 lightdm-deepin-greeter 作为登录界面,greeter 可以通过调用 LightDM 的 api 进行 linux-pam 的认证。

    当认证通过后,LightDM 会启动一个会话(Session),之后运行的程序都将是以登录的用户身份启动,然后 systemd --user 进程会被启动,同时 dbus-daemon 也会启动,之后 LightDM 会启动 startdde。

    此时就开始执行到 DDE 的初始化阶段,startdde 会启动 dde-session-daemon 和 DDE 的核心组件,之后就并行启动 autostart 中的 desktop 文件。

    startdde 目前的流程本身没有问题,问题出在 systemd 接管了 dbus,并且从 systemd 226 版本开始,/etc/pam.d/system-login 默认配置中的 pam_systemd 模块会在用户首次登录的时候, 自动运行一个 systemd --user 实例。 只要用户还有会话存在,这个进程就不会退出;用户所有会话退出时,进程将会被销毁。当#随系统自动启动 systemd 用户实例启用时, 这个用户实例将在系统启动时加载,并且不会被销毁。

    systemd --user 实例是针对每个用户处理的,而不是针对会话。这样做的原理是用户服务处理的大部分资源,像 socket 或状态文件是针对每个用户的(存活于用户的主目录下)而不是会话。这意味着所有的用户服务是独立于会话之外运行的。最终,我们得出结论:基于会话运行的程序可能会导致用户服务中断。systemd 处理用户会话的方式是非常生硬的(pretty much in flux)。

    那么问题就来了,上面说了,systemd 接管了 dbus 服务,通过 dbus 启动的程序会在 dbus.service 下面,成为它的子进程,而 systemd --user 在有用户其它登录的会话存在时,并不会停止 systemd --user,所以 dbus.service 也不会停止。但是 dde 的会话 已经完成注销了,但是通过 dde 启动的程序却没有被退出,从而导致了程序 逃逸

    解决方案?

    如果只是想解决逃逸问题,那么想一个办法,在 session 停止的时候,主动把 dbus.service 服务停掉其实就可以解决了,但是我们想要利用 systemd 的自动依赖解决,这样就不用重复造一点点小轮子了。

    那么需要怎么修改才能符合桌面环境的要求?

    GNOME 和 KDE 现在已经完成了 systemd session 的工作,我们可以在这两个老大哥身上学习。

    提供了一个新的会话入口点 gnome-session-systemd。 这需要一个参数,即开始会话的单元(通常是 .target 单元)。 它将执行一些清理功能,初始化 systemd 环境,然后启动单元并等待它停止。会话通过修改它们的 Exec 行并删除 RequiredComponents 来选择以这种方式启动。

    XDG autostart 现在 systemd 提供了一个 wrapper,可以自动生成出 service 单元,所以功能可以复用。

    在 systemd 管理的系统上,每个用户都被分配了一个 user-x.slice (systemd.slice(5)),并且用户的会话将在 session-Y.scope (systemd.scope(5)) 中运行。您还可以在主机上看到一些其它用户特定的单元,包括 [email protected],它是用户的 systemd 实例。这是用户的一个单独的 systemd 进程,如果用户不再登录,它将再次关闭。

    随着 systemd 的移动,不仅 DBus 激活的应用程序和服务,您的整个会话现在都使用用户的 systemd 实例启动。 这有一些副作用,起初可能看起来很奇怪。 例如,之前提到的 session-Y.scope 曾经包含 200 多个进程,现在减少到仅 4 个进程。 另一个副作用是更难理解进程属于哪个会话(这与许多服务相关)或者 ps 将不再显示 tty。

    但是根据 GNOME blog 的文章说明,这些副作用已经被处理了。GNOME会话仍然始终绑定到session-Y.scope(例如,使用 loginctl kill-session 继续可靠地工作)。

    查看了 GNOME 提供的 dbus 服务单元文件,最终明白了 GNOME 是怎么做到防止 dbus 程序逃逸的,其实操作非常简单,就是提供了一个 gnome-session-shutdown.target,在这个 target 里关联了 gnome-sessino-restart-dbus.service,里面执行的是 gnome-session-ctl --restart-dbus, 其实就是通过 DBus 调用了 systemd --user 的 dbus 方法,将 dbus.service 给停掉了。(- - |)

    改造方案

    既然了解了前因后果,那么我们可以着手改造 DDE 了。

    还需要补充一个知识,会话启动以后,会有一个进程作为会话的入口,会话所有的程序都是从这个入口开始启动的,这个入口就是 startdde

    我们既然希望利用 systemd 的服务来启动,并且帮助我们自动解决依赖,但是拆分项目目前压力比较大,我们还计划重写一部分后端功能,可以在重写时进行调整。

    启动入口

    首先我们需要创建一系列的 servicetarget 文件。

    dde-session-initialized.target
    dde-session-manager.service
    dde-session-manager.target
    dde-session-pre.target
    dde-session-restart-dbus.service
    dde-session-shutdown.service
    dde-session-shutdown.target
    dde-session.target
    dde-session-x11-services-ready.target
    dde-session-x11-services.target
    dde-session-x11.target
    org.deepin.Session.service
    

    看起来非常的多,不用担心,他们都有各自的作用,你可以认为这是将生命周期在 systemd 实现了。

    由于 systemd 会帮助我们自动完成依赖解析,那么我们只需要保留一个入口服务,其它服务都禁止手动启动即可。

    我选择使用 dde-session-x11.target 作为入口,因为未来 deepin 还要支持 wayland,那么在这里就区分开会比较方便一些,因为桌面环境的后续启动与使用什么图形服务没有太大关系。

    根据文件名称就可以方便的了解到这些文件是在什么阶段被执行的。

    退出时清理

    创建的 dde-session-shutdown.target 用来关联所有退出时需要执行的 services

    清理 dbus.service

    提供了一个 dde-session-restart-dbus.service 用来注销以后关闭 dbus.service,不要问,问就是没办法~。

    dde-session-shutdown.target 中会关联这个服务,当用户的会话注销或者桌面环境服务出现问题时,就可以退出所有的 dbus.service

    当发现会话注销时,dde-session-manager.service 会执行退出,在服务关闭时启动 dde-session-shutdown.target,并且使用 replace-irreversibly 标记为不可撤销。

    dde-session-shutdown.target 中又会清理 dbus.service 下的所有程序,这样就避免了服务可以通过 dbus 逃逸出会话。

    架构模型

    最终效果

    可以看到 startdde 和它的进程树挂在 systemd 下面。

    原本的位置现在只有一个占位的程序。

    引用资料

    https://blogs.gnome.org/benzea/2019/10/01/gnome-3-34-is-now-managed-using-systemd/

    原文链接:https://blog.justforlxz.com/2021/12/25/Starting-sessions-with-systemd/

    Thursday, March 11, 2021

    原文:https://emersion.fr/blog/2019/xdc2019-wrap-up

    > XDC 2019 wrap-up 2019-10-10

    这篇文章是我从加拿大蒙特利尔飞往赫尔辛基的路上写的,我刚参加完 X.Org 的开发者研讨会。从去年西班牙那次会议以来,这是我第二次参加 XDC(译者注:X.Org Developer’s Conference)。

    今年的研讨会非常的给力。我遇到了很多一起工作过的同志,他们来自四面八方的各个组织和项目。会议内容也很牛,我会尝试总结一下这次讨论的一些事情。

    libliftoff

    前一阵子,我搞了一个新项目,名字叫 libliftoff。它是我在这次会议上的亮点,所以接下来我会详细的介绍它。带着问题往下看:“libliftoff 是用来解决什么问题的呢?”

    libliftoff 是什么

    它是一个合成器的中间层,负责从 clients 中获取 buffers,把它们绘制在一个独立的 buffer 上,并把这个 buffer 显示到屏幕上。假设我现在打开了一个“文本编辑器”和一个“终端”,合成器会把它们的窗口 buffer 复制到屏幕的 buffer 上,一般是用 OpenGL 做这个事。

    不过,复制 buffers 的操作很浪费资源,因为它会做很多事情,比如:它总是进行 alpha 混合、在不同格式之间转换、让渲染引擎一直干活(注:GPU 在执行 OpenGL/Vulkan 的命令)等等,这些操作不仅占用时间,而且也会增加耗电量。

    为了改善性能,许多 GPU 都提供了硬件实现的“叠加平面(planes)”支持(下文直接称为“叠加平面”)。“叠加平面”可以作为显示引擎负责合成工作,这种行为称为直接显示,可以避免全让合成器做复制 buffers 的活。

    在 Android 上,硬件“叠加平面”在“Hardware Composer”组件中用的很广。许多 Wayland 合成器也会为光标使用“叠加平面”,但是用在其它场景的情况还是很少见。Weston(译者注:一个 Wayland 合成器) 是仅有的使用“叠加平面”的合成器之一。

    使用“叠加平面”并不是一件容易的事情,它有一些限制,比如:不支持 Alpha 通道、不是任意的 buffer 都可以用。虽然这些限制往往会让人觉得很扯淡,但它是由硬件特性决定的。举个例子:对于某些格式的 buffers,Intel 的硬件只能将其放在“叠加平面”的偶数坐标位置、某些 buffers 是在内存上分配的,“叠加平面”干脆就不支持这一类的 buffer、另外,显示设备一般都有带宽限制,在“叠加平面”上使用过大的 buffer 时会失败、某些 ARM 设备上,多个“叠加平面”之间不支持重叠。

    目前为止,关于硬件的这些限制信息,合成器程序还查询不了,因为硬件之间的差异性很大,所以很难设计一个能查询硬件限制信息的 API,甚至在某些新出 GPU 上还会有更奇怪的限制。唯一的方法是搞一些前置工作,用它做一些尝试性的使用。

    因此,设计和实现一个能有效的使用“叠加平面”的代码是一个相当复杂的事情。我一直在想,共享这些代码,是不是会对合成器使用“叠加平面”有所帮助,于是我就奔着这个目标设计了 libliftoff。

    libliftoff 的研讨会

    图一

    我在 XDC 搞了一个研讨会,讨论关于 libliftoff 的事情,目的是把合成器和驱动专家凑一块,一起商量如何才让合成器有效的用上“叠加平面”。

    libliftoff 的受欢迎程度让我很惊讶,连桌子附近都坐满了人:

    • 在合成器这边,有 wlroots 团队的人(Drew DeVault, Scott Anderson 和我自己),KDE 的 Roman Gilg,Weston 的 Daniel Stone,还有 X server 的 Keith Packard。
    • 在驱动这边,有很多来自于不同厂商的开发者:有 AMD、Arm、Google、Nvidia、Qualcomm 等等。

    大家貌似都很兴奋,为 libliftoff 提了很多宝贵的意见。我在此感谢所有参与研讨会的人!

    短期的计划是,让 libliftoff 为合成器提供能实际使用的功能(实验性的),不过有个事我还需要弄清楚,那就是应该怎么样支持多个 output:因为每个 output 都有自己的时序,所以,如何将“叠加平面”从一个 output 迁移到另一个 output 是一件很难搞的问题。最终,如果能实现这样的功能就完美了:为每一个图层分配一个优先级,并把那些频繁在别的图层之前更新的图层放在同一个“叠加平面”上。

    另外也讨论了一些长期计划。

    首先,我们想搞定一个跟内存相关的问题:clients 往往会在内存上分配 buffer,这些 buffer 没法直接用在“叠加平面”上。通常,合成器都会给一些提示信息:“你现在正在使用 Y_TILED,所以我不能将你用在“叠加平面”上,但是如果你能使用 X_TILED 我就能把你用在“叠加平面”上”,这样 client 就可以决定切换它的 buffer 格式。我大概在一年前给 Wayland 提了一个补丁,这个补丁新增的信息能被 libliftoff 识别出来,在这种情况下,合成器可以把这个信息转发给 clients。

    接下来是关于内核 API 的问题,现在只有一个办法能让我们知道是否可以使用“叠加平面”,那就是用 atomic 相关的 test 接口测试(译者注:atomic 是 DRM 提供的一系列原子操作接口,支持事务),这个做法相当的不清真,因为我们必须得遍历尝试多个组合条件,基本上是暴力求解。除此之外,比较好的做法都要跟具体的硬件相关。

    为了搞定这个问题,我们必须让内核提供更多的信息,但是因为不同硬件的差异性很大,设计一个足够通用的 API 将是一个很蛋疼的事情,另一个解决方案是在 libliftoff 中添加特定厂商的插件,允许每个驱动添加自己的代码,以此更好的匹配自己的硬件。目前来说,貌似这是最好的方案了。

    这是本次讨论的总结(PPT):

    视频:XDC 2019 - Day 3

    Scott Anderson 也写了一些总结,是关于 Wayland 协议的一些想法的详细介绍。

    分配器

    在三年前的 XDC 上,Nvidia 的 James Jones 提出了这个分配器项目,用于解决 GBM/EGLStreams 现有的问题。今年,他做了一个关于 GBM、Nouveau 和 transitions 的新的分享。跟之前的提案相比,这个提案的目的是在现有 API 的基础上建立一种叫 transition 的机制。

    视频:XDC 2019 - Day 2

    transition 可以用来减少渲染引擎的带宽占用。某些 GPU 支持使用压缩的 buffer,与未压缩的 buffer 相比,GPU 可以在这些压缩后的 buffer 上直接执行 OpenGL/Vulkan 的操作,这样做效率很高。所以,我们应该在渲染时使用压缩后的 buffers。

    不过,压缩后的 buffers 不能直接用在“叠加平面”上,需要先将它们解压才能使用,有一个方法是把它解压为一个新的 buffer,但是这样需要复制数据,速度会很慢。

    在某些 GPU 上有个更好的方法,它们支持在“叠加平面”中直接解压 buffer。这指的是:当一个 Wayland client 在压缩后的 buffer 上渲染,如果这个 buffer 可以在“叠加平面”中解压,并且之后可以直接使用它,那我们就将这种“buffer 就地解压的程序”称为 transition。

    之后我们在一个研讨会中继续探讨 transitions,讨论了应该在何时何地进行 transition 操作。想法一是:如果合成器准备把一个 buffer 用到“叠加平面”中,那就在合成器中做这个事,可以用 libliftoff 判断什么时候应该使用 transition。另一个想法是:在 client 把 buffer 提交给合成器之前做这个事,client 需要从合成器那获取一个信息,以便知道应该什么时候执行操作(这就是我之前提到的那个 Wayland 协议的补丁可以做的事情)。

    这是这次研讨会的总结:

    视频:XDC 2019 - Day 3

    可变刷新率(VRR)

    AMD 的 Harry Wentland 搞了一个关于“自适应同步”(DisplayPort 的技术)、“可变刷新率”(HDMI 的技术)和“FreeSync”(AMD 的技术)的研讨会。所有这些技术都是为了适配屏幕的功能,允许它从“固定为 60Hz”的模式变成“可以稍微等待下一帧”的模式(译者注:动态刷新率的模式)。

    视频:XDC 2019 - Day 2

    这东西的主要使用场景是游戏应用。游戏通常是用动态刷新率的方式更新画面(依赖于场景的复杂度),下一帧可以更快也可以更慢。游戏还希望能降低延迟,即避免渲染到在屏幕上显示的时间差过长。

    在屏幕固定刷新率的情况下,如果有一帧的渲染用时稍微长了一点,它错过了显示器的刷新时机,这样就会导致这一帧的显示有些滞后,而 VRR 则允许增加屏幕刷新时的等待时间,以免丢帧。

    另一种场景是视频播放。视频有固定的帧率,但是通常会与屏幕的刷新率不同,视频播放器需要用帧插值的方式,才能适应屏幕的刷新率,而 VRR 则允许降低刷新率以匹配能完美播放视频的时序。

    屏幕内容通常都是处于静止状态,在这种情况下,VRR 还可以降低电量消耗。比如用户在使用文本编辑器时,完全不需要 60FPS,这时候合成器可以降低屏幕的刷新率。

    游戏的使用场景比较简单,合成器可以比 deadline 稍微晚一点点提交帧,硬件可以搞定这种情况。其它场景需要做更多的工作,要想把 VRR 用在视频播放器上,我们需要某种定时提交帧的 API。要想用 VRR 做节能,合成器要能支持帧率改变时的平滑过渡,否则屏幕会有闪烁问题(这是支持 VRR 功能的屏幕的限制)。

    在我看来,我们目前应该把精力放在 Wayland 协议对游戏场景的支持上,因为搞其它两种场景需要做的事情太多了,它们不仅需要更多的新 API(内核和 Wayland 的都需要),还需要做很多的实验工作。

    Chamelium

    我在 Intel 实习期间,参与了一个叫 Chamelium 的项目,它是一个基础的屏幕模拟器,你可以通过网络向它发送命令,它已经被用在了 ChromeOS 的 i915 显卡环境的 CI 服务中。

    我搞了一个关于这个项目以及我在项目内的工作的研讨会:

    视频:XDC 2019 - Day 3

    除了这三个主题,我们还讨论了很多其它的东西,但是不能全塞到这一篇博客中讲,所以就说到这吧。感谢所有参加 XDC 的人,感谢 Mark Filion 组织了这次活动,还要感谢所有赞助商,没有你们的赞助就不会有的这次活动!

    Tuesday, September 3, 2019

    还记得刚推出屏幕保护功能那会儿,我偶逛论坛,围观大家对这个功能的评价。其中让我印象最深的一句话就是:“一股Windows98风”,总之,评价总结出来就是一个字:“吃藕”。

    大家追求美好事物的诚挚之心深深地打动了我,而且,我个人做事情的风格是喜欢未雨绸缪,在屏幕保护程序开发之初,就已经定好了易于扩展的架构,所以我当时就下定了决心,为大家开发一个非Windows98风格的屏保。

    为了达到绝对“非Windows98”的目的,我特意选择了Windows10中的默认屏保作为参考,在无数个周末的战斗下,最终成功将名为“泡泡”的屏保应用发布到了商店(项目地址:https://github.com/zccrs/screensaver-pp )。

    本着“授人以鱼不如授人以渔”的理念,特地整理了这篇文章协助大家开发一款属于自己的时尚屏保应用。

    正文

    在Linux+X11生态环境中,xscreensaver是最“流行”的屏幕保护程序,有着非常多的屏保资源,所以deepin-screensaver必然要兼容它的资源。

    但是,xscreensaver对屏保资源的扩展方式并不符合deepin的开发理念,因此,deepin-screensaver实现了一套全新的屏保扩展方式。

    支持使用Qt qml模块编写屏保应用,一个标准的屏保应用只需要包含一个 “xx.rcc” 文件,将文件安装到 /usr/lib/deepin-screensaver/resources目录。

    rcc 格式是一个编译之后的Qt资源文件,在这个资源文件中至少要包含两个文件:qml代码文件、屏保封面图。

    image

    图中文件名括号内为其别名,也就是屏保主应用加载文件时能读取到的文件名。

    • qml代码文件:屏保应用的代码入口,会被屏保主程序加载显示

    • 屏保封面图:设置屏保入口显示的预览图,支持svg png jpeg bmp等格式 所有的文件必须以特定的目录结构组织到一个Qt资源文件(qrc文件),以“泡泡”屏保为例:qml.qrc 为其资源文件,包含三个前缀路径

    • /deepin-screensaver/modules:放置屏保应用的主qml文件,此路径下的所有qml文件都会被当做一个独立的屏保应用,因此,项目中的其它文件需要额外建立新的前缀放置

    • /deepin-screensaver/modules/cover:放置屏保应用封面图文件,文件名称必须和modules目录中的qml文件一致,且包含它的 “.qml” 后缀。如图上,qml文件全名为:“pp.qml”,封面图全名为:“pp.qml.svg”。

    • /deepin-screensaver/modules/pp:此前缀不是必须的,用于放置项目中的其它文件。为了不与其它项目产生冲突,建议使用项目名作为目录名称 资源文件最好以项目名称命名,避免和其它屏保应用冲突。另外,大家可能已经发现了,这三个前缀都有一个共同点,那就是以 “/deepin-screensaver/modules"开头,的确,这是一个格式要求,不能随意更改路径。

    主qml文件作为屏保应用的入口,它的根元素一定要设置

    anchors.fill: parent
    

    这样才能确保屏保应用充满整个屏幕。在多屏的情况下下,会创建多个窗口示例,可根据屏幕绘制不同的屏保内容。 项目编译其实很简单,只需要使用Qt提供的rcc命令将qrc文件编译为rcc文件即可,使用qmake构建系统时,可以在pro文件中调用以下命令:

    system(rcc --binary $$_PRO_FILE_PWD_/xx.qrc -o $$_PRO_FILE_PWD_/xx.rcc)
    

    当然,最后不要忘记将 xx.rcc 文件安装到deepin-screensaver所要求的目录。做完这所有的步骤后,回到桌面,在右键菜单中选择“壁纸与屏保”,切换到屏保设置后即可看到新添加的屏保应用。 另外,deepin-screensaver为qml提供了获取当前屏幕截图的接口,只需要为Image项指定特定的路径即可:

    Image {
        anchors.fill: parent
        source: "image://deepin-screensaver/screen/" + Screen.name
    }
    

    由于要获取屏幕名称,上述代码需要 “import QtQuick.Window 2.2” 使用

    后记

    屏保封面图最佳比例为:8:5,推荐使用svg格式,以更好的适应高分屏缩放。

    推荐大家使用Qt Creator作为项目的开发工具,可以方便的编辑 qrc 文件。

    泡泡屏保是一个完整的demo,有任何疑问的地方都可以以其作为参考

    参考

    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”属性。

    推荐