• 首页
  • 加入
  • RSS
  • Thursday, August 24, 2023

    应用大模型

      AIGC 是基于大模型的,而大模型的基础是深度学习。上一篇文章对深度学习进行了初步介绍,首先是深度学习的神经元起源,引发了基于线性函数的模拟,又因为线性函数无法习得逻辑异或,因此引入了非线性的激活函数,再通过三层神经网络给出了MNIST手写数字识别的模型,接着又介绍了神经网络是如何通过数据与反向传播来学习与调整参数的,最后给出了神经网络的分层结构。

      大模型的直观应用当然首先体现在包括ChatGPT、文心一言、讯飞星火等问答型产品的使用上,另一方面也体现在编程上,在此先给出大模型的编程应用。以下使用的模型、库与样例均来自于Hugging Face。

      图1给出了基于大模型的英中翻译代码与运行结果。从图1中可以看到,真实的翻译代码只有14、15两行,其逻辑是使用了Helsinki-NLP的opus-mt-en-zh模型,其中mt代表机器翻译(machine translation)、en和zh分别表示英文和中文。从图1中同样可以看到,翻译结果相对还是比较准确的。

    图1 基于大模型的英翻中

      图2给出了基于大模型的文本情感分析的代码与运行结果。从图2中可以看到,实际有用的代码也仅需14、16两行,而且这次没有指定具体模型,只给出了需要text-classification这种模型。代码运行结果是认为文本情绪是负面的(NEGATIVE),准确度大概是90.1546%,这显然是符合实际的,因为文本是对商家发错货的抱怨。

    图2 基于大模型的文本情感分析

      图3给出了基于大模型的问答。这次的代码稍多一点,但实际的代码也只有三行。第14行给出了需要一个问答(question-answering)的大模型,但是没有指定大模型的名字,第15行是提问的字符串“What does the customer want”,即用户到底想要什么。第16行则使用上述文本作为上下文,提问字符串作为问题,传给问答大模型获取答案。从运行结果看来,答案还是蛮靠谱的。

    图3 基于大模型的问答

      当然,基于大模型的程序还有很多,但是从上面三个例子已经可以看出,基于大模型可以写出简短而强大的自然语言处理的程序,下面让我们走进大模型,看看它究竟是如何做到这一点的。

    走进大模型

      大模型在自然语言处理领域里大放异彩,因此首先需要了解自然语言的特点。

      自然语言的显著特点(也是难点),那就是词与词之间有着广泛的关联。比如下面两句英文:

    • Go to the bank to get some money.
    • Go to the bank to get some water.

      只有看到每一句的最后一个词,才能分辨出 bank 到底是银行还是堤坝。再比如下面这两句中文:

    • 今天太冷了,能穿多少穿多少。
    • 今天太热了,能穿多少穿多少。

      整句话唯一不同的就是冷与热这两个字,但也就是这句话的一字之差,就导致了整句话的意义完全不同了。

      因此,自然语言处理的关键点就在于如何能准确地判断词与词之间的关系,如果能准确地知道所有词之间的关系,那即使缺了一个词,也能根据关系推出缺的词应该是什么词。当下处理这一问题的主流技术是transformer,这个词不好翻译,主要因为它和变形金刚的英文一模一样。transformer的核心概念是注意力(attention),即每个词到底在注意其它的哪个词,或者说哪些词之间有什么关系。

      注意力具体由以下关键概念组成:

    • 每个词(实际上是词元,token)均有对应的 q/query(查询)、k/key(键值)与 v/value(值)这三个矩阵变量
    • q[i]用来查询本词(i)与其它词(j)之间的注意力
    • k[j]是词j回应查询的键值,具体是q[i]与k[j]相乘后缩放,接着用softmax激活函数处理,再乘以v[j],这就得到了词i针对词j的注意力att[i,j]。
    • 词i的对应输出为计算得到的注意力之和,即 y[i] = att[i,1] + att[i,2] + … + att[i,n]
    • 注意力可以有多个(multi-head),每个注意力可以关注不同的方向,例如有的注意力关注的是词与词之间的意义,有的关注的是押韵,等等

      图4给出了一个句子中各个词注意力的计算过程。

    图4 注意力计算过程

      句子是“小明、小刚、小红是小强的朋友,小明是…”,当前的词是“小红”,序号是3,可以看到小红的q值与每个词的k值相乘之后再用softmax处理(缩放操作在这里省略了),接着再与每个词的q值相乘,最后相加即可得到序号为3的输出。

      那么这些k、q、v 等的值如何确定呢?当然是通过上一篇文章里提到的反向传播进行学习的,那反向传播学习自然语言的正确输出是什么呢?在大语言模型中,其训练手段是使用大量的高质量语料,将词语按序逐批输入大模型,以原句子中的下一个词或者特意被空缺出来的词为正确输出来学习的。例如在上面的句子中,大模型在输入了“小明、小刚、小红是小强的”之后,应该能计算输出“朋友”这个词,如果输出错误,则通过反向传播调整各个参数。也就是说,大模型就是根据一个词之前的词或者周围的词是什么来计算出这个词的,这就是大模型的理论基础和学习方法。

      注意力虽然是大模型的核心概念,但除此之外,大模型还用到了其他技术,它们包括:

    • 第一步需要将词转为数值向量(vector)的嵌入层(embedding),这也是现在很火的向量数据库的那个向量,向量数据库就是用来查询哪些文本向量比较相似,从而提取出对应的文字材料的
    • 给词向量加上含位置信息的位置编码,以分辨出词的先后顺序,显然“我爱你”与“你爱我”的意义截然不同,因此词序是很重要的,而注意力本身并没有用到词序
    • 在注意力层之后的残差处理与归一化处理,技术原理并不难,感兴趣的读者可以看参考资料

      现在我们已经对大模型的主要技术原理有了粗浅的了解,下面来看看深度学习与大模型的特点。

    大模型的特点

      从对深度学习以及大模型的初步应用与原理了解,其实我们已经可以得出一些结论。

      首先,深度学习的算法模型是神经网络及其节点上的参数/权重构成的,深度学习是端到端的机器学习,它不用由人类专家预先设计特征算法,例如不用苦思冥想怎么设计一个提取图像中线条的算法,只需要用大量的高质量标注数据对其进行投喂,就有可能最终训练出质量不错的神经网络(中的各个参数)来。

      其次,它是一个黑盒子,因为没有特征设计,因此这么多数值构成的神经网络,它为什么能达到最终的目标,为什么某个参数就必须是最后训练出来的那个数值,为什么有的时候识别会失败或者生成会出错,是否可以手工修改某些特定的参数使得其成功率上升,同时又不会导致原来成功的事例失败,这些问题在现阶段都是难以回答的,这就是深度学习的可解释性问题。

      在ChatGPT之前,大模型其实也在发展,但是ChatGPT表现出来的多领域通用性和流畅性与之前的产品相比有明显的超越,所以造成了轰动。其后的GPT-4表现更为优异,在《Sparks of Artificial General Intelligence:Early Experiments with GPT-4》这篇论文里,微软团队给出了这样的一个示例:

    提问:Can you write a proof that there are infinitely many primes, with every line that rhymes?
    GPT4:Yes, I think I can,
    Though it might take a clever plan.
    I’ll start by noting Euclid’s proof,
    Which shows that primes aren’t just aloof.
    Assume we have a finite list,
    Of primes, and that none have been missed.
    Multiply them all together,
    And add one, just to be clever.
    The result will be a number,
    That has no prime factors, oh wonder!
    But every number has a prime,
    That divides it, it’s just a matter of time.
    So we’ve found a contradiction,
    And our finite list needs eviction.
    There must be infinitely many primes,
    And that’s the end of my rhyming lines.
    

      提问者让GPT-4写一首诗,来证明素数有无限多个,然后GPT-4很快就完成了这首诗,每两行押韵,而且用欧几里得的经典方法给出了证明。当然,在论文中还有很多让人印象深刻的例子,但是上面的例子已能说明GPT-4确实有了很强的能力。

      GPT中的G表示生成(Generative),这表示它主要的工作方式是生成内容,内容在这里主要是自然语言文本。按照OpenAI首席执行官Sam Altman的说法,The most important difference between ChatGPT and GPT-4, is that the GPT-4 predicts the next word with greater accuracy,即 ChatGPT与GPT-4最大的差异就在于GPT-4在预测下一个词的准确度比ChatGPT更高。

      GPT中的P表示预训练(Pretrain),即首先用大量语料训练出基础模型(foundation model),然后再用下游任务相关的语料进行精调(FT,即finetuning)。这些下游任务可能是文本分类、翻译、对话等等,这样就可以不用单独为某个特定任务从头训练了。我们可以把中学教育和通识教育看作是预训练,它为大学最终的专业选择,以及以后更细的工作分工打下了坚实的基础。反过来看,在小学年龄阶段没有上学可能会对以后的择业带来很大的限制,这也可以看成是大脑在应预训练的阶段没有进行有效的预训练导致的问题。

      GPT中的T表示transformer,这个已经在上文中介绍了。

      一般认为,GPT-4有更强能力的原因在于:

    • 它提供了大量的高质量数据,原始数据有45T,清洗后的语料是570G,清洗比例接近1%,这是之前几乎没有团队做到的
    • 数据中混合了大量的代码,原始数据中有830G代码,这一般被认为是推理能力提升的关键点之一,当然另一方面也大大提升了它的代码能力
    • 展开了大量不同种类的下游任务,如生成、问答、脑暴、闲聊、摘要、分类、提取等等,以上两点也属于多样化工作,它为GPT4的通用性打下了基础
    • 使用了基于人工反馈的增强学习(RLHF)方法,召集了40个众包团队,撰写了数十万的提示数据以对齐主流价值观

      一般认为,大模型的表现之所以如此智能,但是之前的小模型神经网络却那么智障,其原因可能在于涌现(emergence)。涌现可以简单认为是单个个体微观上简单的行为,在宏观上大量复合呈现出难以预料的规律。比如每只蚂蚁其行为其实是挺简单的,但是一群蚂蚁在一起,就可以表现出复杂的规律。又如每个神经元的行为都很简单,但是这么多神经元聚集在一起,就形成了聪明的人类大脑,这也算是一种涌现。

      涌现最直观的例子可能就是康威的生命游戏(Conwey’s Life Game)了,这个游戏是在一个网格平面(类似围棋棋盘)上发生的,每个个里要么有一个存活的细胞,(用黑色格表示),要么就是一个死亡的细胞(用白色格表示),其规则也很简单,只有以下四条:

    • 当前细胞存活时,当周围存活细胞<2时,该细胞死亡(模拟生命数量稀少)
    • 当前细胞存活时,当周围有2个或3个存活细胞时,该细胞保持存活
    • 当前细胞存活时,当周围存活细胞>3时,该细胞死亡(模拟生命数量拥挤)
    • 当前细胞死亡时,当周围存活细胞=3时,该细胞复活(模拟繁殖)

      那么图5里的四个样式就表示绝对静止的细胞群体。

    图5 康威生命游戏中绝对静止的细胞群体

      对每个样式进行分析很快就会知道为什么它们会绝对静止。以第二个样式为例,其每个存活细胞周围都刚好有两个存活细胞,按照规则2,它们都应该保持存活。而任何一个死亡细胞周围都没有四个存活细胞,因此此样式将永远不变。

      图6给出了震荡循环的细胞群体。

    图6 康威生命游戏中震荡循环的细胞群体

      按照康威生命游戏的规则,可以发现图6中的两个样式会演变几步之后又变成当前的样式。以第二个样式为例,其一共有左中右三个存活细胞。左侧与右侧的存活细胞附近只有一个存活细胞,因此按照规则1,会在下一轮死亡。中间的存活细胞附近有两个存活细胞,因此按照规则2,保持存活。同时又可以发现,中间的存活细胞上侧和下侧的死亡细胞由于其附近有三个存活细胞,因此根据规则4,在下一轮它们将复活。以此类推,样式会由横三转为纵三,又转回横三,永远震荡循环。

      康威生命游戏有着远超过上述样式的复杂度,在宏观上甚至可以看到游走、巡回、扩张、凋零等多种细胞社群的样式,因此四条简单的微观规则就衍生出了让人事先难以预料的宏观样式上的复杂度,是涌现的一个生动形象的例子。

      以ChatGPT与GPT-4为代表的大模型由于其使用了自然语言对话而引发了轰动,让普通人都能直观感受到大模型的魅力,但同时它作为一个基础设施,也提出了一个难题,就是它的编程接口是基于自然语言的,所以需要做所谓的提示工程(prompt engineering)。所谓提示工程,指的就是想让大模型好好干活,那就需要自己好好琢磨怎么和大模型好好说话。俗话说见人下菜,或者说见人说人话,见鬼说鬼话,那见了ChatGPT,当然就得说ChatGPT话了,不然它就没法理解问题,自然也没法给出好的回答了。Linux圈子里有Linus大佬的一句名言:“talk is cheap, show me your code”,中文翻译也很传神:“废话少说,放码过来”,俗一点的话那就是“少哔哔,秀代码”,但是自打GPT横空出世,以后可能就是“code is cheap, show me your talk”了,毕竟,给GPT一个提示,它可以还你百行代码。

      不过神经网络毕竟是一种信息压缩,或说是一种函数拟合,因此中间肯定会有信息损失,或说是自己瞎想的填补空白,那就避免不了GPT一本正经的胡说八道,也就是所谓的幻觉(hallucination)了。幻觉是当前大模型应用的主要障碍之一,一般认为,大模型近期的发展将沿着消减幻觉、工具集成(即能使用外部工具)、多模态(即除了文本以外,也能理解和生成图形、语音、视频等内容)、垂直领域、类脑智能、具身(embodied)智能等方向发展。

    Monday, August 21, 2023

    背景

      从去年底以来,AIGC 炙手可热,多个业界大佬都认为 AIGC 会给整个产业带来一场革命,甚至所有的软件都会用 AI 重写。从历史上来看,人机交互方式的变革往往会将操作系统带入下一个世代,著名的例子如从命令行界面的 DOS 到键鼠图形界面的 Windows,以及带来触控界面的 iPhone,领创者都成为了世界顶级企业,带动了整个生态的发展。

      从技术上来看,AIGC 是基于大模型的,而大模型的基础是深度学习,因此,为了在产品上结合 AIGC,首先从技术上首先需要对深度学习进行有深度的学习。

      对深度学习与大模型的探索将由一系列文章组成,本文是系列里的第一篇,主要关注的是深度学习的技术入门探索。

    从神经元开始

      回溯历史,深度学习起始于向人类的大脑学习如何学习。人类大脑皮质的思维活动就是通过大量中间神经元的极其复杂的反射活动,因此不妨先看看神经元的工作机制。

    图1 神经元结构

      图1给出了神经元的大体结构,左边是神经元的主体,其输入是左侧的多个树突,其输出是右侧的一个轴突。只有当输入树突的信号足够强烈的时候,输出轴突上才会有信号产生。受此启发,就可以设计一个最简单的有两个输入x1与x2,以及一个输出y的线性函数来模拟单个神经元,引入阈值θ,当 w1x1 + w2x2 ≥ θ时,y为1(表示有信号),否则y为0(表示无信号)。其中w1与w2分别是x1与x2的参数或权重(weight)。

      有了这个函数,下面来看看它究竟能做什么。按照逻辑主义的设想,数学可以通过逻辑推衍出来,那么不妨看看,上面的函数是否可以表征出基本逻辑运算,如与、或、异或等,在这里x1、x2与y的取值都只能是0或1。

      对于逻辑与来说,只有当x1与x2都是1的时候,y才是1,否则y是0,容易尝试得到一组可能的w1、w2与θ,分别是0.5、0.5与0.7,如图2所示。

    图2 逻辑与的线性函数图

      图2中横轴为x1,纵轴为x2,从图2中可以看到,(1, 1) 点为实心圆,表示y为1,在(0, 0)、(0, 1)与(1, 0)都是空心圆,表示y为0,中间的虚线表示w1x1 + w2x2 = θ这条直线,只要这条直线能将(1, 1)点与其它点划分到不同区域,则显然就可以找到至少一组w1、w2与θ满足条件。基于同样的分析,容易知道逻辑或也可以找到对应的w1、w2与θ。但是对于逻辑异或来说,问题就严重了,显然无法找到满足条件的w1、w2与θ,如图3所示。

    图3 逻辑异或的函数图

      逻辑异或是当x1与x2中一个为0,另一个为1时y才为1,否则y为0,因此在图3中,点(0,1)与点(1,0)为实心圆,而(0, 0)与(1, 1)为空心圆,显然是无法找到一条直线将两个实心圆与两个空心圆划分在两个不同区域的。因此,上述最朴素的线性神经元函数无法表示逻辑异或,也就意味着有大量的运算无法通过上述线性神经元函数来进行。

    引入激活函数

      是否能改造上述函数,让它能支持所有运算,从而能承担学习的任务呢?至少,人脑肯定是能学会异或的。现在看来,主要是因为原始的神经元函数太线性导致的这个问题。因此,在深度学习中,就引入了非线性的激活函数(activation function),如图4所示。

    图4 引入激活函数

      在图4中,首先原函数被修改成了支持多个输入和多个输出的线性变换函数,这样就能处理更多种类的问题了。因为有了多个输入x1、x2…xm与多个输出h1、h2…hn,因此权重的下标也带有两个数字,以表示每个权重的作用,例如 w12 是输入x2与输出h1间的权重。还有一个特殊的权重bi,它被称为偏置(bias),是一个待确定的常数项。这样,h就等于相应的x与w相乘后再加上b。例如,hi = xiwi1 + x2wi2 + … + xmwim + bi。   经过线性变换后得到的输出h1、h2…hn只是中间过程的输出,在之后,还需要加入一个非线性的激活函数的处理,以得到最终的输出y1~yn,如图4所示。

      在具体激活函数的选择上,比较常见的有 softmax、sigmoid 与 relu 等。其中 softmax 函数是多分类问题最常用的输出激活函数(多分类问题指的是一个问题有多个确定个数的可能答案,例如是/否问题是二分类问题,而分辨一个手写阿拉伯数字是哪个数就是一个十分类问题,因为可能答案有0~9一共十个),softmax也是包括ChatGPT在内的大模型使用的输出函数。   使用了激活函数以后,神经网络就可以学习到所有函数了。下面来看一个经典的神经网络的例子,手写数字识别问题,或MNIST问题。MNIST涉及的手写数字在网上是公开的,如图5所示。程序员们可以先想想,如果自己来写一个程序识别手写数字会怎么写。可以识别手写数字的(一个)神经网络的结构如图6所示。

    图5 MNIST手写数字样例

    图6 能识别手写数字的神经网络

      可以看到图6的神经网络一共用到了三个线性变换,并使用了两个sigmoid 激活函数,以及最后的softmax激活函数,因此可以说这个神经网络是三层的。神经网络的输入(x1~x784)是一个长度为784的数组,其实就是一个28x28=784的手写数字的黑白图像。神经网络的输出(y1~y10)分别代表了0~9的阿拉伯数字,这是一个典型的十分类问题,因此使用softmax也是非常自然的。

      图6中的神经网络一共有(784x50+50) + (50x100+100) + (100x10+10) = 45360个参数,对比ChatGPT上千亿个参数,这显然是一个微模型,但是它的识别能力却可以达到92.53%,也就是说一万个手写数字,它能正确识别出9253个来。   那问题就来了,这45360个参数是怎么来的呢?肯定不能是随便什么 45360 个数都能带来这么高的识别率的,要解决这个问题,就需要看看神经网络是怎么学习的了。

    神经网络的学习

      在上面已经看到,神经网络里有大量的参数。在最开始,这些参数会被随机分配一些数字(当然如何随机分配也有讲究的,简洁起见,此处先不提),此外也需要准备大量的数据,这些数据一般是多个输入输出的对(x, t)。例如在上面的手写数字识别问题中,输入x就是一个28x28的手写数字图像,输出t就是这个图像对应的0~9中的一个数字。   这些数据会被分成训练集与测试集。训练集中的数据用来训练神经网络,让神经网络中的参数最终达到正确的值。测试集中的数据用来测试训练后的神经网络,对比看训练后的神经网络在新的数据下得到的结果是否正确。

      神经网络的训练过程可以大体分为下面几步:

    • 对训练集中的输入输出对(x, t)进行如下处理

    • 将x输入到神经网络中,计算得到y

    • 将y与正确的输出t进行运算得到损失L,损失的计算函数一般是均方差或交叉熵,前者针对的是回归问题(连续函数拟合),后者针对的是分类问题

    • 根据L调整神经网络的参数,调整的方向是减少L,调整的方法是下面要讲的反向传播

      图7给出了神经网络训练的过程。

      图7 神经网络训练过程

        一旦训练完毕,使用的时候就不需要正确输出t,也不需要计算损失L和调整神经网络的参数了,这个过程被称为推理(inference),如图8所示。

      图8 神经网络推理过程

        顺便说一句,图中的深度神经网络与神经网络结构是一样的,但是层数较多,因此被称为深度神经网络。

        下面,再来看看神经网络究竟是怎样通过损失L来调整网络参数的。最简单,也是最直观的方法就是将每个参数都稍微调大或者调小一点,看L会如何变化,如果L变小,则保持此参数的调整,如果L变大,则将此参数反过来调整。以上即正向调整法,思路清晰,操作方法简单,但是计算量极大,因为每调整一个参数就要重新计算一遍y与L。

        另一种方法就是现在主流的反向传播(BP,backpropagation)法,此方法类似系统发生故障时的根因分析,首先分析最后一层的参数是怎样影响到L的,然后分析倒数第二层的参数是如何影响到最后一层的输入的,如此类推。在数学上,其实就是计算L对某个特定参数w的(偏)导数,因为导数就代表了w的变化会导致L如何变化。根据链式求导法则,L对w的导数等于L对中间变量h的导数乘以h对w的导数,前者相当于计算最后一层参数的导数,后者相当于计算倒数第二层参数的导数,两者相乘即为L对导数第二层参数的导数。

        下面主要通过求导来展示反向传播,如果希望更直观一点,可以阅读计算图相关的资料。假设真实函数是y=2x+1,则待求函数为wx+b(当然w与b的真实值应该是2与1)。下面通过一组数据(训练集)来通过反向传播逐步计算更新w与b,看看它们否会逐渐逼近2与1。

        由于这是一个回归问题,因此使用均方差(y-t)< sup>2< /sup>/2作为损失L的函数,显然L对y的导数是y-t,参数更新使用经典的梯度下降法(SGD),即参数新值=参数旧值 - 学习率x(L对参数的导数),梯度下降有一个粗糙但是直观的理解,那就是学习应该向着导数(梯度)相反(下降)的方向走,在这里学习率这个参数设为0.01。

        首先,将w与b随机化为0.5与0.6。

        假设第一个训练对为(0, 1),则 y = wx + b = 0.5·0 + 0.6 = 0.6,L对w的导数=L对y的导数乘以y对w的导数=(y-t)·x=(0.6-1)·0=0,L对b的导数=L对y的导数乘以y对b的导数=(y-t)·1=-0.4。则w的新值为w-0.01·0=0.5,b的新值为b-0.01·(-0.4)=0.604,显然新的w与b比原来的更接近(2, 1)。

        若第二个训练对为(1, 2.9)(本来应为1与3,但是增加了一点误差干扰),可以以同样的方法得到新的w为0.51796,而新的b为0.62196,显然比上一对w与b又接近了2与1一点。

        实际上,若继续增加2x+1附近的数据,可以发现到了十几对训练数对之后,w与b即可相当接近2与1了。

        以上例子是为了直观感受反向传播的计算而给出的,实际上这种线性函数的回归可以通过数据集基于矩阵一次性算出来,而且训练本身也要考虑收敛的问题,因此实际的深度学习会更复杂一些,但是原理是类似的。

        总地来说,深度神经网络是由多个层组成的,每一层均有前向(forward)推理的函数,用来从输入计算得到输出,这个过程即为推理。每一层也有反向(backward)传播的函数,用来从后一层传来的导数计算得到本层向前一层传递的导数,并同时更新本层的参数。如果是训练,则需要在最后一层再加上一个输入为t与y的损失层,输出为L,如图9所示。

      图9 多层神经网络结构

        通过以上几乎标准化的神经网络层,深度学习的研究者就可以像搭积木一样对多个层进行排列组合,得到多种多样的深度神经网络,并首先通过反向传播训练出神经网络的参数,继而使用神经网络进行推理应用了。

    Monday, August 14, 2023

    队员:复旦大学 朱元依、沈扬、朱俊杰
    指导老师:张亮、陈辰
    企业导师:王子冲

    本项目为2023年操作系统大赛企业赛道赛题。

    项目链接:DDE 控制中心自启动管理插件 github仓库

    1 摘要

    Deepin(原名Linux Deepin)致力于为全球用户提供美观易用,安全可靠的Linux发行版。该系统由深度科技自主开发,提供了美观易用、极简操作的桌面环境,主要由桌面、启动器、任务栏、控制中心、窗口管理器等组成。其中,控制中心是Deepin桌面环境的核心组件之一,它是用于管理和配置操作系统各种设置的集成工具。然而,在该控制中心中,并未对用户提供自启动项提供的便捷管理界面。在本项目中,我们为Deepin操作系统中自启动项的修改功能编写了简洁易用的控制中心插件,将自启动管理的系统功能集成到了控制中心中。该插件以单独的仓库提供,并能够单独构建,一键植入Deepin控制中心中。

    2 需求分析

    最终用户对自启动权限的管理目前只能通过dde-launcher(启动器/“开始菜单”)的右键菜单进行管理,而控制中心作为控制系统的门户应用反而缺少此功能。

    用户需求

    在官方发布的deepin23-Beta版本中,对于用户程序与系统程序的自启动管理方法为:找到应用的可执行程序(通常为.desktop)类型的文件,通过右键打开功能菜单的方式设置为开机自启动。但由于操作系统自带的应用程序界面中所有程序都会被展示到,所以当操作系统中应用程序过多的时候用户很难统计到那些程序被设置成了开机自启动,同时对于用户不想参与管理的应用也会展示在应用菜单中。因此我们对于本项目开发所面向的需求是在控制中心(deepin-control-center)中在不影响原有插件所提供的设置服务的基础上,为用户提供一个额外的插件用于管理开机自启动软件,同时插件可以满足用户自行选择需要手动维护的自启动程序,对于用户希望自己管理是否自启动的软件显示在面板上可以对是否自启动进行开关,用户不希望管理的软件默认不自启动不显示到面板;面板也可以提供添加和删除的功能让用户挑选出自己想要在面板上操作的应用。

    功能需求

    deepin作为国产开源的深度Linux桌面系统,不仅为用户提供了人性化、个性化以及对于中文等语言有良好支持的操作系统体验,也为Linux的开发者与学习者提供了控制中心(dde-control-center)与任务栏(dde-dock)等桌面控件的开发者接口与插件注入接口。除了可以不断的扩展完善用户需求与用户体验外,deepin深度桌面在操作系统学科的教学与深入理解方面也带来了很多的可能性与创造性。因此作为OS大赛的参与团队,我们不仅是在希望我们的开发会对deepin项目的完整性、deepin用户的体验感上带来一些帮助,同时也想通过我们自己的努力在学生的视角让操作系统的教学与后续学习有一个优秀的案例和一些开发相关的经验总结,使得我们理论层面的操作系统教学可以有更大的实践空间。

    2.1 当前方案

    目前已有的自启动管理方法是在开始菜单中对菜单中所展示应用软件单独进行自启动的设置。具体的方法是对所希望设置自启动项的应用软件选中后右键,点击“设置开机自启动”即可在每次开机时自动打开该应用软件。

    默认自启动设置方式

    然而,这种方法有两大明显的缺陷:(1)无法向用户展示所有的自启动项设置(2)大批量的自启动项修改极其不便。由此,催生了控制中心自启动管理插件的需求。

    2.2 插件需求

    为了完成控制中心插件,我们对需求进行了更细致的刻画。经过总结后,插件的需求主要分为三条:

    1、完成一个控制中心插件,能够展示当前所有开机启动项的列表; 2、能够在插件中,通过用户界面的交互来管理(添加、删除、启用、禁用)开机启动项; 3、插件以单独的仓库提供,并能够单独构建,不需要合并入 dde-control-center 项目。

    其中,第一条需求是该插件的基础。自启动项的列表一方面为用户提供了清晰的展示界面,另一方面也是程序与用户交互,获取修改操作信息的基础。 第二条需求总结了该插件需要支持的功能,即添加、删除、启用、禁用,这些功能需要在前端设计对应的交互界面,同时在后端设计对应的操作接口,调用系统接口以修改自启动设置。 第三条需求与系统发布相关。目前最新的稳定发布版本是Deepin V23 Beta,而官方版本已经在发布,若修改源码统一编译控制中心,会需要对当前已发行的操作系统版本进行修改,较为不便。故需要单独编译该插件,并将其装载到系统中的插件接口中。

    3 相关资料调研

    3.1 Deepin开机自启动系统设置

    Deepin系统通过检测固定的目录,检测自启动项。通过放置应用程序的.desktop文件在其中一个自动启动目录中,系统可以检测到该应用程序的自启动设置。通过修改.desktop文件中的对应字段,可以修改对应应用程序的自启动设置。

    3.1.1 自启动目录

    “desktop base directory specification”中的“Referencing this specification” 部分进行定义了自动启动目录是 $XDG_CONFIG_DIRS/autostart

    如果同一文件名位于多个自动启动目录下,只应使用最重要目录下的文件。

    示例:

    如果未设置$XDG_CONFIG_HOME,用户主目录中的自动启动目录为~/.config/autostart/

    如果未设置$XDG_CONFIG_DIRS,系统范围的自动启动目录为/etc/xdg/autostart/

    如果未设置$XDG_CONFIG_HOME$XDG_CONFIG_DIRS,并且两个文件/etc/xdg/autostart/foo.desktop~/.config/autostart/foo.desktop存在,那么只有文件~/.config/autostart/foo.desktop将被使用,因为~/.config/autostart//etc/xdg/autostart/更重要。

    3.1.2 应用程序的.desktop 文件

    一个应用程序的.desktop文件必须符合"桌面入口规范"中定义的格式。所有关键字应按照定义进行解释,但以下情况除外,以便考虑到位于自动启动目录中的.desktop文件不会显示在菜单中。

    Hidden关键字

    .desktop文件的Hidden关键字设置为true时,该.desktop文件必须被忽略。当多个具有相同名称的.desktop文件存在于多个目录中时,仅应考虑最重要的.desktop文件中的Hidden关键字:如果其设置为true,则其他目录中具有相同名称的所有.desktop文件也必须被忽略。

    OnlyShowInNotShowIn关键字

    OnlyShowIn项可以包含一个字符串列表,用于标识必须自动启动此应用程序的桌面环境,其他桌面环境不得自动启动此应用程序。

    NotShowIn项可以包含一个字符串列表,用于标识不得自动启动此应用程序的桌面环境,其他桌面环境必须自动启动此应用程序。

    这两个关键字中的一个,要么是OnlyShowIn,要么是NotShowIn,可以出现在单个.desktop文件中。

    TryExec关键字

    带有非空TryExec字段的.desktop文件如果TryExec关键字的值与已安装的可执行程序不匹配,则不得自动启动。TryExec字段的值可以是绝对路径,也可以是没有任何路径组件的可执行文件名。如果指定了没有任何路径组件的可执行文件名,则会搜索$PATH环境以找到匹配的可执行程序。

    注意事项

    如果通过在系统范围的自动启动目录中安装.desktop文件来自动启动应用程序,则个人用户可以通过在其个人自动启动目录中放置具有相同名称的.desktop文件来禁用此应用程序的自动启动,并在其中包含Hidden=true关键字。

    3.2 控制中心插件开发

    V23控制中心特性

    1、V23控制中心只负责框架设计,具体功能全部由插件实现; 2、V23控制中心支持多级插件系统,支持插件插入到任意位置中; 3、高度可定制,可定制任意插件是否显示,若插件支持,可定制任意插件内容是否显示。

    V23控制中心插件安装路径说明

    1、控制中心会自动加载翻译,翻译目录需要严格放置在/${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/dde-control-center/translations下,控制中心会自动加载,同时,插件的翻译和名称也有要求,命名为${Plugin_name}_{locale}.tslocale就是多语言的翻译,翻译文件必须控制和插件名称相同; 2、控制中心的so应该放置在/${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTA;LL_LIBDIR}/dde-control-center/modules下,请使用构建系统的提供的gnuinstall路径,上面举的例子是cmakemesonbuild也有自己的逻辑。

    V23控制中心开发接口说明

    1、ModuleObject类用于构建每个页面元素,其是插件的核心; 2、PluginInterface类用于规范插件信息,每个插件必须提供一个`ModuleObject对象。

    标准开发流程示例

    1、继承PluginInterface,实现其虚函数; 2、实例化一个根模块,根模块在初始化时不允许有耗时操作,若有耗时操作,应继承ModuleObject然后实现active方法,将耗时操作放入其中; 3、若根模块的子项是横向菜单列表,则可使用List储存其基础信息,继承或使用HListModule类,然后循环使用appendChild方法将菜单添加到根模块中; 4、若根模块的子项是纵向菜单列表,则可使用List储存其基础信息,继承或使用VListModule类,然后循环使用appendChild方法将菜单添加到根模块中; 5、以此类推,具体的某个子项菜单同样再次添加菜单列表,直到菜单列表的子项为PageModule时为止; 6、准备一个以上的Module继承自ModuleObject,并实现其page()方法,然后添加到PageModule中,注意,page()方法中需返回新的QWidget对象; 7、当某个菜单为PageModule时,使用其appendChild方法将上方的Module添加到其子项中,此时,控制中心会根据page的大小添加滚动条,并将多个page进行垂直排列进行显示。PageModule持支嵌套,并且其有默认边距,如果嵌套使用,嵌套的PageModule边距建议设置为0; 8、若某个VListModulePageModule页面需要附加按钮时,可调其子项ModuleObjectsetExtra,该ModuleObjectpage提供按钮,这样该ModuleObject将显示在VListModulePageModule页面的最下方。

    4 系统框架设计

    4.1 项目组织方式

    类图

    类图

    项目文件组织

    .
    ├── CMakeLists.txt
    ├── include
    │   ├── interface
    │   │   ├── ...
    │   └── widgets
    │       ├── ...
    ├── misc
    │   ├── ...
    ├── shell.sh
    ├── src
    │   ├── frame
    │   │   ├── ...
    │   ├── interface
    │   │   ├── ...
    │   ├── plugin-selfstartup
    │   │   ├── operation
    │   │   │   ├── ...
    │   │   └── window
    │   │       ├── ...
    │   └── widgets
    │       ├── ...
    └── translations
        ├── ...(translation files)
        ├── desktop
            ├──...(desktop translation files)
    

    4.1 总体思路与系统框架

    4.1.1文件框架思路

    通过阅读dde-control-center的源代码,src部分为插件的实现以及实现代码复用所存在的框架代码,代码的插件部分在src/plugin-selfstartup目录,分为operation和window部分,其中windows部分主要负责控制中心自启动项目的界面构成,operation部分负责参与控制中心对系统文件与配置的控制。translation文件夹为控制中心(dde-control-center)提供了不同语言环境下的支持,通过识别系统的语言环境选择展示到面板不同的语言。include与misc为项目注册到控制中心所必须包含的编译依赖文件。

    4.1.2 设计思路

    我们在初赛中已经完成了通过注册到dde-dock实现的自启动插件,对于插件的界面以及界面上按钮对应的功能已经有了一个大致的构想,但由于dde-dock的插件是一个较为独立的结构,而dde-control-center里面的每一个插件都需要往控制中心里注册一个module并且通过rpc方法与dde-application-manager进行远程服务的交流,因此在实现细节上dde-control-center插件和dde-dock插件有着很大的区别。

    用户逻辑方面,我们首先在控制中心实现了管理自启动应用的面板,对于添加到维护列表的应用都会被展示到面板上,对于每一项应用都会提供Enable/Disable的选择按钮和Delete的删除按钮,用于管理是否开机自启动与不希望维护改应用的删除。同时在面板有一个添加按钮,用户点击后会打开文件对话框,用户可以自行从中选择自定义路径的应用程序添加到插件维护的列表中。

    插件运行逻辑方面,分别有添加、删除、开启/关闭(反转)逻辑,分别思路如下:

    • 添加逻辑:用户在点击添加按钮后会获取该文件的路径并读取FileInfoAddButtonWidget发送requestCreateFile信号携带参数Category名称与FileInfoWorker中;Worker/home/user/.config/autostart文件下判断是否存在一个相同的应用信息,如果不存在则创建一个原.desktop文件的副本并且添加一行Hidden=false字段,把应用信息存到App结构体中,同时调用Worker中对应的Category的添加函数传入赋值好的App,Category在内存中同步一份自启动应用信息,最后更新前端页面。
    • 删除逻辑:在用户点击应用对应行的删除按钮后获取到AppID,通过getAppById得到App信息后发送requestDelUserApp信号到WorkerWoker找到autostart文件夹中对应文件并将其删去,同时把App在对应的Category中移除,最后更新前端页面。
    • 反转逻辑:在用户点击对应行的打勾按钮后想获取到AppId,通过getAppById得到App信息后发送反转请求到WorkerWorker读取磁盘中autostart里对应.desktop文件文本找到Hidden字段并将其反转,同时调用Category把内存中App结构体的Hidden成员反转,最后更新前端页面。
    • 启动逻辑:插件启动的时候会调用CategorygetAppItem函数,该函数从autostart文件夹中逐文件读取信息存在App结构体中,封装到m_appList作为初始化时参与维护的应用程序,前端从m_appList中把应用名称以及是否自启动信息列举到页面

    4.2 类功能说明

    operation部分
    ├── defappmodel.cpp
    ├── defappmodel.h
    ├── defappworker.cpp
    ├── defappworker.h
    ├── mimedbusproxy.cpp
    ├── mimedbusproxy.h
    └── qrc
    

    operation部分是插件的后端部分,对于控制中心的每个插件都有ModelWorkerDBusProxy三个部分:

    • ModelModel部分通过继承QObject注册到QT的项目中,私有变量Category实现了自启动信息在内存中的一个副本用于前端的交互。
    • WorkerWorker部分提供了插件对文件系统的操作。由于操作系统对于开机自启动的支持在于把对应的.desktop文件拷贝到/home/user/.config/autostart中并设置Hidden=false,因此对于自启动应用管理的插件必须要对文件的读写提供支持,该支持由Woker部分实现。
    • DBusProxy:由于插件要注册到控制中心并且对应用进行管理,因此需要向运行中的应用程序管理服务(dde-application-manager)进行交互,管理服务提供了rpc的调用接口,插件通过DBusProxy部分向管理服务发起远程请求。
    window部分
    ├── selfstartup.json
    ├── selfstartup.cpp
    ├── selfstartupdetailwidget.h
    ├── selfstartupplugin.cpp
    ├── selfstartupplugin.h
    └── widgets
        ├── addbuttonwidget.cpp
        ├── addbuttonwidget.h
        ├── category.cpp
        └── category.h
    

    window部分是插件的前端部分,由PluginDetailwidgetAddbuttonwidgetCategory四个部分:

    • PluginPlugin部分构造了自启动程序插件。包括插件接口的初始化,一级页面的初始化和二级页面的初始化。
    • DetailwidgetDetailwidget部分构造了自启动程序插件的 app条目。包括app条目的外形、位置,app条目的增删改查操作,以及与workermodel的交互操作(通过信号和槽函数实现)。
    • AddbuttonwidgetAddbuttonwidget部分构造了自启动程序插件的加号按钮。包括加号按钮的外形、位置,新增app的弹窗显示,新增app的路径处理,以及与workermodel的交互操作(通过信号和槽函数实现)。
    • Category: category部分为磁盘中autostart文件夹中重要信息在内存中的拷贝,用于插件的窗口部分直接获取到该文件夹中.desktop类型文件的重要字段展示到界面。

    4.3 实现描述

    4.3.1 DefAppModel

    名称功能
    DefAppModelModel构造函数
    ~DefAppModelModel析构函数
    getModSelfSetUp返回SelfSetUp内存Category

    4.3.2 DefAppWorker

    名称功能
    DefAppWorkerWorker构造函数,连接Worker和Model
    DefaultAppsCategory枚举类,用于实现插件的可扩展性,实现对不同类型软件的分类,默认状态为只有SelfSetUp类
    active向应用程序管理服务发出blockSignal(false)消息
    deactive向应用程序管理服务发出blockSignal(true)消息
    onReverseUserApp对参与维护的自启动应用开关反转处理,把autostart中的.desktop文件Hidden字段反转并同步Category
    onGetListApps与Model处理应用变化信号结束的信息提供的一个空接口,只用于承接信号处理
    onDelUserApp在autostart文件夹中删去用户不希望继续维护是否自启动的应用并同步Category
    onAddUserFile向autostart中添加用户希望维护是否自启动的应用并同步Category
    getCategory返回应用类型的分类

    4.3.3 MimeDBusProxy

    名称功能
    MimeDBusProxyDBusProxy构造函数。
    DeleteApp向应用程序管理服务发送删除App请求。
    AddUserApp向应用程序管理服务发送添加用户App请求。
    ListApps向应用程序管理服务发送展示所有App请求。
    Change向应用程序管理服务发送App变动处理请求。

    4.3.4 SelfStartupDetailWidget

    名称功能
    SelfStartupDetailWidget自启动软件条目窗口构造函数。初始化条目窗口中的文字不可编辑、icon大小、条目形状、条目不可移动,初始化存储软件列表的QStandardItemModel,初始化软件条目的布局。
    ~SelfStartupDetailWidget自启动软件条目窗口析构函数。
    setModel设置自启动软件条目窗口的当前model。根据当前窗口的分类,设置不同的窗口model(由于本插件目前只有一个分类,因此setModel功能相当于直接调用setCategory功能)。
    setCategory设置自启动软件条目窗口的当前分类。将分类的增、删、改的信号和对应的自启动软件条目窗口的槽函数连接,将分类中的软件放入存储软件列表的QStandardItemModel中,并更新自启动软件条目窗口。
    updateListView更新自启动软件条目窗口。依次读取自启动软件条目窗口的当前model中的每一个软件状态,依照软件状态,更新窗口显示(显示是否自启动、软件名称、软件icon、删除按键)。
    getAppIcon获取软件的icon。从系统中获取软件的icon,并统一调整为32*32大小。
    getAppById通过ID获取APP结构体。遍历分类中的app,返回对应的APP结构体。
    appendItemData向model中新增app信息。从APP结构体中获取app信息,向model中新增app,并更新总app数量。
    isDesktopOrBinaryFile判断文件是否属于桌面或二进制文件。
    isValid判断app是否有效。判断app的ID非空。
    reverseItem向category发出app自启动状态转换的信号。
    requestDelUserApp向category发出删除app的信号。
    onListViewClicked自启动软件条目窗口被点击后的槽函数。从自启动软件条目窗口获取app信息,并向category发出app自启动状态转换的信号。
    onDelBtnClicked自启动软件条目窗口删除按钮被点击后的槽函数。从自启动软件条目窗口获取app信息,并向category发出删除app的信号。
    onClearAll清空model中所有的app信息。
    getAppListview返回model中所有的app信息。
    AppsItemChanged重置model中所有的app信息。依次将app_list中的app信息存入model中,并连接激活、点击信号。
    onReverseAppcategory返回app自启动状态转换信号的槽函数。更新对应model中app的自启动状态,并更新窗口。
    addItemcategory返回app新增信号的槽函数。向model中新增对应的app信息,并更新窗口。
    removeItemcategory返回app删减信号的槽函数。向model中删减对应的app信息,并更新窗口。
    showInvalidText设置自启动软件条目窗口的字体、图标的位置、大小。

    4.3.5 SelfStartupPlugin

    名称功能
    SelfStartupPlugin自启动程序插件的构造函数。基于DCC_NAMESPACE::PluginInterface的接口。
    name返回自启动程序插件的名称。
    module自启动程序插件初始化函数。初始化自启动程序插件的一级页面、二级页面和加号按钮。
    location返回自启动程序插件的位置。即,在控制中心插件中的排序。

    4.3.6 SelfStartupModule

    名称功能
    SelfStartupModule自启动程序插件一级页面的构造函数。初始化一级页面的名称,描述,图标,work,model。
    ~SelfStartupModule自启动程序插件一级页面的解构函数。向m_work、m_model发送删除信号。
    work返回m_work。
    model返回m_model。
    active激活m_work。

    4.3.7 SelfStartupDetailModule

    名称功能
    SelfStartupDetailModule自启动程序插件二级页面的构造函数。初始化二级页面的名称,分类,work,model。
    page自启动程序插件二级页面的初始化函数。初始化DetailWidget,并将DetailWidget的app状态修改信号、删除信号与work的槽函数相连接。

    4.3.8 Category

    名称功能
    CategoryCategory类构造函数,继承QObject类,每个Category类里面封装相同类型的应用信息
    getName获取当前Category分类的名称
    setCategory设置当前Category的类型名称
    getappItem获取当前Category封装的应用信息
    clear清空当前Category储存的应用信息
    addUserItem把传入应用信息存到Category中并向前端发送更新信号
    delUserItem把目标应用从Category中删除并向前端发送更新信号
    reverseUserItem设置目标应用Hidden字段反转并向前端发送更新信号

    5 系统测试情况

    5.1前端测试

    完成插件安装后,可在 DDE 控制中心中看到名为“自启动程序”的管理选项:

    前端页面

    5.2 自启动管理功能测试

    点击改图标,可以进入自启动管理界面:

    自启动管理页面

    该界面展示了设置所有的自启动项的软件,如果在软件右边出现了蓝色勾,则说明该软件设置了开机自启动。可以通过点击蓝色的勾取消开机自启动的设置,再次点击则可恢复。由此实现了自启动管理的开启、禁用功能。

    底部的加号用于添加需要进行自启动管理的软件,点击后跳出选择窗口:

    应用选择页面

    进入软件所安装的文件夹,选择该软件,即可将其添加入自启动管理中。在自启动管理界面中,可以点击软件右侧灰色的叉号,以将该软件剔除出自启动管理的界面。由此实现了自启动管理的添加、删除功能。

    6 插件的可扩展性

    本次项目的插件完整的覆盖了OSCOMP-proj223的所有需求,但由于只是一个短期开发的项目,因此有构想到了很多可以对插件进行扩展的方面;同时deepin作为开源社区也为开发者提供了良好的扩展接口,我们考虑了以下几点的扩展路径:

    6.1 语言扩展

    对于translation 板块,我们修改和编译仅仅使用了dde-control-center的zh_CN部分,限于语言广度我们只能提供插件的简体中文和英文模式,对于deepin可以支持的其他语言还有待扩展

    6.2 用户体验扩展

    由于Linux系统贯彻了"everything is a file"这一思想,因此整个操作系统的磁盘布局是文件化的,用户的应用可以安装到磁盘的任意地方,同时考虑到大部分Linux使用者为具有计算机基础能力的开发者,我们在插件中设计的添加应用按钮是让用户自主找到文件路径并添加。但是对于一般用户,这样的操作可能有些许复杂,所以我们考虑一个提高用户体验的方式:deepin中系统应用和从应用商店获取的用户程序分别分布在了两个文件夹中,如果用户没有自定义路径的话,我们的插件可以在用户请求添加的时候扫描这两个文件夹,提前给用户展示出可能用户希望添加的应用程序可供直接点击添加

    6.3 实时性扩展

    我们测试了dde-control-center的大部分插件,我们发现几乎所有都没有实现实时扫描磁盘的功能——即如果我们在dde-control-center外部对插件所管理的磁盘进行了修改的话,前端的界面并不会感受到更新而刷新界面。以自启动为例,我们实现了管理插件,但是用户仍然可以在应用菜单栏从控制中心外部添加到自启动中,此时自启动并不会更新页面,需要放回上一级窗口重新启动插件进行扫描。对于这一项扩展,我们考虑了一种方法为新起一个线程不停的扫描autostart文件夹中的.desktop文件并与Category中的比较,如果出现了不一致则发送(Q_Emit)更新窗口的信号,这样可以实现不返回上一级的情况下更新。但该功能只是作为扩展性的一个构想,实际实现的话首先带来的用户体验收益并不是很大,且已经存在插件的情况下应用场景很小,同时开启一个持续扫描的线程对于内存与插件的效率有较大的开销。

    6.4 并发性扩展

    插件的工作流程包含了修改磁盘与更新内存中的映射同时展示到面板上。对于磁盘的修改会涉及到一定次数的I/O,因此时间开销一定会比内存中信息更新更大的。因此我们思考了可以在此插件的基础上在新起一个进程,用rpc的方式对插件提供服务,服务内容包括对autostart文件夹的增删查改,插件只需要修改内存中的部分并作为rpc客户端向磁盘I/O的服务发送函数调用请求即可

    6.5 一致性的扩展

    因为插件会同时修改内存与磁盘的信息,并且保证二者相同,此时就会存在一定的一致性问题:

    • 磁盘和内存写入不同步,在其中一个进行的过程中发生了崩溃程序退出,此时是否成功写入成为一个一致性问题。我们的解决方式为先写入磁盘再写入内存,同时在重启插件的时候会重新扫描一次磁盘,这样只要写入磁盘的第一阶段成功之后即使崩溃也可以在重启的时候恢复数据
    • 多进程同时操作:如果有多个控制中心进程同时进行了文件的操作,采取的方式为读磁盘,写磁盘,写内存的操作,这样可以使得在读磁盘的时候如果有同名的应用被添加到里面会被正在写入(包括添加与删除操作)的进程阻塞读取,在写入过程结束之后才会触发读磁盘,然后写内存的时候判断是否存在,如果存在则跳过第三步,如果不存在则再写入磁盘

    附录A 插件安装

    1 开发环境配置

    1.1 配置 Deepin 操作系统

    开发环境:Deepin V23Beta版

    系统架构:x86

    镜像下载链接:https://mirrors.ustc.edu.cn/deepin-cd/releases/23-Beta/

    虚拟机平台:WMware Workstation 16Pro

    操作系统环境搭建参考博客:https://blog.csdn.net/qq_44133136/article/details/105887560

    1.2 配置 Deepin 插件环境

    安装依赖包

    sudo apt build-dep .
    sudo apt install -y qt5-default
    sudo apt-get install dde-control-center-dev
    

    1.3 插件安装测试

    安装插件:
    sudo sh install.sh
    

    安装成功后,打开控制中心,会看到以下自启动插件图标,即为安装成功:

    控制中心插件图标

    此时,如果进入/usr/lib/x86_64-linux-gnu/dde-control-center/modules/文件夹,看到编译出的.so文件已经被下载到该文件夹中:

    插件安装位置文件夹

    1.4 插件卸载测试

    卸载插件:
    sudo sh uninstall.sh
    

    重启控制中心,可以看到原本的“自启动管理”图标消失,即为卸载成功。

    附录B 开发过程问题记录

    1 开发环境配置问题

    1.1 Deepin V20 Beta与Deepen V20.9

    在开发环境配置过程中,我们遇到了比较大的问题。

    我们起初顺延初赛的思路进行开发,在Deepin V20Beta版中进行开发。我们完成相关依赖包的安装后可以顺利地编译我们所写的控制中心自启动插件。但我们发现安装插件后的控制中心只剩我们所编译的自启动插件。(如下图中仅剩测试编译的 Default Applications 和 Self Start-up 插件)

    失败编译结果-仅剩插件

    我们推测是因为系统自动配置的控制中心框架与网上的控制中心开源代码版本不同,导致插件安装后框架复写。故我们只需重新编译控制中心的代码重新安装至虚拟机系统中即可完成测试。

    然而,在尝试编译控制中心时各种尝试均失败。报错DTK版本不符,未找出解决方法。在Deepen V20.9中亦未成功。

    1.2 Deepin V23Beta

    在遇到上述问题后,我们加入了Matrix的Deepin开发者社区,在线上与其他开发者讨论我们所遇到的问题。

    经过与其他开发者的讨论,我们得知最新的开源版本的控制中心已不再支持 20 版的Deepin系统。

    于是我们重新安装最新版本的Deepin V23Beta,成功的将我们所编写的插件植入控制中心中。

    2 图标与文字颜色展示问题

    我们初步的demo展示出来的界面所有管理的应用程序都是统一显示的默认应用程序的图标,但我们还是希望要和deepin桌面上.desktop文件相同的图标;与此同时,我们发现发现插件中的应用程序名字都是红色,同时希望这里是一个经典的黑色,这两个都是关于item的展示问题。我们关注到了这一段代码:

    QIcon icon = getAppIcon(iconName, QSize(32, 32));
    act->setIcon(icon);
    act->setTextColorRole(DPalette::TextTitle);
    act->setIconText(name);
    
    • 关于icon问题,我们可以对App结构体进行扩展,增加一条icon字段,通过读取带有图标应用程序.desktop文件中的icon字段存到当中,如果没有icon字段的程序设置为“application-default-icon”,并且在展示的时候调用act->setIcon()传入icon字段
    • 关于颜色问题,上述代码中看到了act->setTextColorRole()字段,里面传入了文字颜色类型的变量,在DPalette中定义了如下常量,初始选择的红色文字为TextWarning,我们只需要替换为TextTitle即可
    TypeName颜色类型
    ItemBackground列表项的背景色
    TextTitle标题型文本的颜色
    TextTips提示性文本的颜色
    TextWarning警告类型的文本颜色
    TextLively活跃式文本颜色(不受活动色影响)
    LightLively活跃式按钮(recommend button)背景色中的亮色(不受活跃色影响)
    DarkLively活跃式按钮(recommend button)背景色中的暗色,会从亮色渐变到暗色(不受活跃色影响)
    FrameBorder控件边框颜色
    NColorTypes无颜色类型

    3 翻译问题

    翻译主要涉及到:插件名的翻译和自启动软件名的翻译。

    对于插件名的翻译。我们学习了DDE-NETWORK-CORE仓库的做法,首先我们编写翻译成不同语言的.ts翻译文件,使用CMakelists中的qt5_add_translation指令获得.qm翻译文件,再将翻译的.qm文件install进入${CMAKE_INSTALL_DATAROOTDIR}/dde-control-center/translations(通常是$/usr/share/dde-control-center/translations),在通过loadTranslator载入系统对应语言的翻译文件。

    对于自启动软件名的翻译。考虑到部分软件缺失官方翻译名称,无法软件的信息中直接获取,我们目前只采用英文格式。

    4 开机启动项处理思路问题

    为了实现插件可视化管理软件的开机自启动,了解Deepin系统开机自启动的功能实现是至关重要的。

    通过查阅资料与实践,我们了解到Deepin系统包含自启动文件夹~/.config/autostart,该文件夹类似于 Windows 下的启动文件夹,系统开机时会执行该文件夹下的每个 desktop 文件 Exec 参数指向的脚本或可执行文件。

    为了确认可行性,小组进行了该方法的验证。首先,通过Deepin系统自带的修改开机自启动设置的方法,修改开机启动项(图中修改终端的自启动项):

    默认自启动设置方式

    随后检查自启动文件夹~/.config/autostart

    yang@yang-PC:~/.config/autostart$ ls
    deepin-terminal.desktop org.deepin.browser.desktop
    

    发现deepin-terminal.desktop文件被添加入了该自启动文件夹中,并且会在开机时自启动。

    而取消终端的自启动时,~/.config/autostart中已有的deepin-terminal.desktop文件并不会被移除,而是其中的Hidden的字段会被修改为false,表示取消开机自启动设置。

    由此,可以通过在插件中检查所有~/.config/autostart文件夹中.desktop文件的Hidden字段来搜索系统所有的自启动软件;也可以通过添加.desktop文件、修改Hidden字段的方式进行开机自启动设置的修改。

    5 rpc远程通信问题

    1. 远程过程调用: RPC允许一个程序调用另一个程序在远程机器或进程上执行的过程(函数或方法),就像本地调用一样。这样,开发者可以透明地在分布式系统中调用远程的功能,无需关注底层网络细节。
    2. 通信协议: RPC通信需要定义一个协议,该协议规定了消息的格式、编码方式、序列化和反序列化方法等。通过确定的request和response协议格式制定规定了客户端和服务器之间如何进行数据交换。
    3. 序列化和反序列化: 在RPC通信中,数据需要在网络上传输,因此需要将数据进行序列化(编码)以便在网络上传输,然后在接收端进行反序列化(解码)以还原数据。序列化和反序列化过程确保数据的正确传输和解释。
    4. 数据传输: RPC通信依赖底层的网络传输协议,如TCP或HTTP。客户端和服务器通过网络传输消息,以实现远程过程调用。
    5. 错误处理: RPC通信中需要处理各种可能的错误情况,例如网络连接中断、超时、服务不可用等。合理的错误处理可以保证系统的可靠性和鲁棒性。
    6. 服务注册与发现: 在dde-application-manager中以字符串命名注册了远程调用函数的服务,dde-control-center中关于对应用的变更可以通过rpc远程调用实现就和
    7. 安全性: RPC通信涉及跨网络的数据传输,因此安全性是一个重要考虑因素。加密、认证和授权等机制可以确保通信的安全性。

    6 插件描述缺失问题

    添加描述前

    在加载插件后,我们发现插件缺失了描述信息,与其他的插件格式明显由很大的不同,通过Matrix上交流发现,是插件缺少了setDescription的代码,因此,正确加入描述后,我们获得了以下结果。

    添加描述后

    附录C 开发计划

    第一步(4/9~4/18)

    • 调研Deepindde-dockQT框架等相关内容
    • 设计项目方案
    • 分工

    第二步(4/19~5/2)

    • 搭建主体插件类的框架
    • 设计启动项管理窗口的前端展示页面

    第三步(5/3~5/13)

    • 开发部件类接口
    • 完善插件类功能

    第四步(5/14~5/21)

    • 插件类右键功能开发
    • 完成配置文件

    第五步(5/22~5/31)

    • Debug
    • 撰写文档

    (以上DDE-Dock自启动插件开发均在初赛完成,项目地址见末尾)

    第六步(6/26~7/8)

    • 调研DDE Control Center框架等相关内容
    • 设计前端界面
    • 分工

    第七步(7/9~7/15)

    • 编译教程中的Hello World控制中心插件
    • 设计插件架构

    第八步(7/16~7/22)

    • 配置环境,编译V20示例插件
    • 设计后端接口
    • 修改windowoperationcategory下的文件

    第九步(7/23~7/29)

    • 配置环境,编译Default-AppSelf Start-up插件
    • Debug

    第十步(7/30~8/10)

    • 修改翻译、文字颜色问题
    • 撰写文档

    附录D 参考资料

    文档

    Qt 插件标准:https://wiki.qt.io/Plugins

    deepin V23 dde-control-center文档:dde-control-center: dde-control-center (linuxdeepin.github.io)

    dde-control-center控制中心插件开发示例:控制中心插件 - deepin开发者平台

    qt-5手册:https://doc.qt.io/qt-5/

    deepin 应用自启动说明:https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html

    博客

    关于deepin开机自启动项的讨论:https://bbs.deepin.org/zh/post/169824、https://blog.csdn.net/qq_21137441/article/details/124825726

    仓库

    dde-control-center仓库:https://github.com/linuxdeepin/dde-control-center.git

    其他开发者的插件项目:https://github.com/linuxdeepin/dde-network-core/tree/master

    说明:由于控制中心的插件对于外观的统一性具有较高的要求,因此,我们仓库中的include/interfaceinclude/widgetssrc/interfacesrc/widgetssrc/frame下的文件均来自dde-control-center源代码仓库,以保证插件接口的一致性和外观的统一性。

    Sunday, June 25, 2023

    KWin 是 KDE 开发的窗口管理器,提供了非常丰富的插件,可以对功能进行大量的定制。

    本篇文章是对窗口特效插件的开发介绍。

    插件开发

    插件定义

    KWin 的插件通常可以使用一些宏辅助生成代码,例如使用 KPluginFactory 进行插件的定义,内容是用来生成插件的入口类。

    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
    #define EffectPluginFactory_iid "org.kde.kwin.EffectPluginFactory" KWIN_PLUGIN_VERSION_STRING
    #define KWIN_PLUGIN_FACTORY_NAME KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME
    #define KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, enabled ) \
    class KWIN_PLUGIN_FACTORY_NAME : public KWin::EffectPluginFactory \
    { \
    Q_OBJECT \
    Q_PLUGIN_METADATA(IID EffectPluginFactory_iid FILE jsonFile) \
    Q_INTERFACES(KPluginFactory) \
    public: \
    explicit KWIN_PLUGIN_FACTORY_NAME() {} \
    ~KWIN_PLUGIN_FACTORY_NAME() {} \
    bool isSupported() const override { \
    supported \
    } \
    bool enabledByDefault() const override { \
    enabled \
    } \
    KWin::Effect *createEffect() const override { \
    return new className(); \
    } \
    };

    #define KWIN_EFFECT_FACTORY_ENABLED(className, jsonFile, enabled ) \
    KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, enabled )

    #define KWIN_EFFECT_FACTORY_SUPPORTED(className, jsonFile, supported ) \
    KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, return true; )

    #define KWIN_EFFECT_FACTORY(className, jsonFile ) \
    KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, return true; )

    大部分宏只是为了方便结构修改,我们只需要使用 K_PLUGIN_FACTORY 进行插件定义即可。

    假设我们开发了一个插件,名字叫 demo,我们只需要在 main.cpp 中使用 KWIN_EFFECT_FACTORY_SUPPORTED 定义

    1
    2
    3
    4
    5
    KWIN_EFFECT_FACTORY_SUPPORTED(
    Demo,
    "metadata.json",
    return true;
    )

    代码展开后是这样的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME : public KWin::EffectPluginFactory \
    {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID EffectPluginFactory_iid FILE "metadata.json")
    Q_INTERFACES(KPluginFactory)
    public: \
    explicit KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME() {}
    ~KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME() {}
    bool isSupported() const override {
    return true;
    }
    bool enabledByDefault() const override {
    return true;
    }
    KWin::Effect *createEffect() const override {
    return new Demo();
    }
    };

    可以看到,其实 KWIN_EFFECT_FACTORY_SUPPORTED 只是为我们生成了工厂函数,辅助生成了一些必要的重载。

    metadata.json 文件是用来作为插件的描述信息使用的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "KPlugin": {
    "Category": "Accessibility",
    "Description": "Allow clip of window content",
    "EnabledByDefault": true,
    "Id": "scissor",
    "License": "GPL",
    "Name": "ScissorWindow",
    "Name[zh_CN]": "窗口圆角"
    },
    "org.kde.kwin.effect": {
    "enabledByDefaultMethod": true
    }
    }

    特效插件

    特效插件是一类可以改变窗口画面的插件,例如我们可以在插件里对窗口进行贴图、变形和裁切,在 DDE 中,就使用特效插件完成了圆角裁切和窗口模糊。

    这里使用圆角裁切插件作为例子,首先使用 KWIN_EFFECT_FACTORY_SUPPORTED 宏对插件进行定义, KWIN_EFFECT_FACTORY_SUPPORTED 接受一个 class 作为返回的接口类,它需要继承自 Effect,第二个参数是元信息的 json 文件,第三个参数是返回是否支持,在启用插件时可对当前环境进行判断,例如插件需要使用 OpenGL 对图形进行一些操作,但是当前环境不支持 OpenGL,那么插件就不会启用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include "scissorwindow.h"

    namespace KWin
    {

    KWIN_EFFECT_FACTORY_SUPPORTED(ScissorWindow,
    "metadata.json.stripped",
    return ScissorWindow::supported();)

    } // namespace KWin

    #include "main.moc"

    在 Effect 类中有几个不同阶段的方法可以重载。

    • prePaintScreen
      • 设置是否变换窗口或整个屏幕
      • 更改将要绘制的屏幕区域
      • 做各种内务处理任务,比如初始化你的效果变量
        用于即将到来的绘画过程或更新动画的进度
    • paintScreen
      • 在窗口上画东西(调用后画 effect->paintScreen())
      • 绘制多个桌面和/或同一桌面的多个副本
    • postPaintScreen
      • 在动画的情况下安排下一次重绘,不应该在这里画任何东西。
    • prePaintWindow
      • 启用或禁用窗口的绘制(例如启用最小化窗口的绘制)
      • 将窗口设置为半透明
      • 设置要转换的窗口
      • 请求将窗口分成多个部分
    • paintWindow
      • 做各种转换
      • 改变窗口的不透明度
      • 改变亮度和/或饱和度,如果支持的话
    • postPaintWindow
      • 在动画的情况下为单个窗口安排下一次重绘
        不应该在这里画任何东西。
    • paintEffectFrame
      • 在绘制 EffectFrame 之前直接调用此方法。
      • 如果需要绑定shader或者执行,可以实现这个方法帧渲染前的其他操作。
    • drawWindow
      • 可以调用以绘制一个窗口的多个副本(例如缩略图)。
      • 可以在这里改变窗口的不透明度/亮度/等,但不能做任何转换。
      • 在基于 OpenGL 的合成中,框架确保上下文是最新的

    在方法名称中可以看出,在场景及窗口绘制的过程中,分别可以在实际绘制的前后分别执行一些动作,圆角插件就是在 drawWindow 函数中,使用 OpenGL 对窗口使用着色器进行窗口裁切,并绘制到屏幕上。

    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
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    void ScissorWindow::drawWindow(EffectWindow *w, int mask, const QRegion& region, WindowPaintData &data) {
    if (w->isDesktop() || isMaximized(w)) {
    return effects->drawWindow(w, mask, region, data);
    }

    QPointF cornerRadius;
    const QVariant valueRadius = w->data(WindowRadiusRole);
    if (valueRadius.isValid()) {
    cornerRadius = w->data(WindowRadiusRole).toPointF();
    const qreal xMin{ std::min(cornerRadius.x(), w->width() / 2.0) };
    const qreal yMin{ std::min(cornerRadius.y(), w->height() / 2.0) };
    const qreal minRadius{ std::min(xMin, yMin) };
    cornerRadius = QPointF(minRadius, minRadius);
    }

    if (cornerRadius.x() < 2 && cornerRadius.y() < 2) {
    return effects->drawWindow(w, mask, region, data);
    }

    const QString& key = QString("%1+%2").arg(cornerRadius.toPoint().x()).arg(cornerRadius.toPoint().y()
    );
    if (!m_texMaskMap.count(key)) {
    QImage img(QSize(radius.x() * 2, radius.y() * 2), QImage::Format_RGBA8888);
    img.fill(QColor(0, 0, 0, 0));
    QPainter painter(&img);
    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(255, 255, 255, 255));
    painter.setRenderHint(QPainter::Antialiasing);
    painter.drawEllipse(0, 0, radius.x() * 2, radius.y() * 2);
    painter.end();

    m_texMaskMap[key] = new GLTexture(img.copy(0, 0, radius.x(), radius.y()));
    m_texMaskMap[key]->setFilter(GL_LINEAR);
    m_texMaskMap[key]->setWrapMode(GL_CLAMP_TO_EDGE);
    }

    ShaderManager::instance()->pushShader(m_filletOptimizeShader);
    m_filletOptimizeShader->setUniform("typ1", 1);
    m_filletOptimizeShader->setUniform("sampler", 0);
    m_filletOptimizeShader->setUniform("msk1", 1);
    m_filletOptimizeShader->setUniform("k", QVector2D(w->width() / cornerRadius.x(), w->height() / cornerRadius.y()));
    if (w->hasDecoration()) {
    m_filletOptimizeShader->setUniform("typ2", 0);
    } else {
    m_filletOptimizeShader->setUniform("typ2", 1);
    }
    auto old_shader = data.shader;
    data.shader = m_filletOptimizeShader;

    glActiveTexture(GL_TEXTURE1);
    m_texMaskMap[key]->bind();
    glActiveTexture(GL_TEXTURE0);
    effects->drawWindow(w, mask, region, data);
    ShaderManager::instance()->popShader();
    data.shader = old_shader;
    glActiveTexture(GL_TEXTURE1);
    m_texMaskMap[key]->unbind();
    glActiveTexture(GL_TEXTURE0);
    return;
    }

    如果窗口是桌面类型,或者已经最大化了,则无需处理,直接返回 Effect 原本的处理函数。

    之后尝试从窗口属性中取出圆角大小的值,如果没有设置圆角大小,或者值小于2,则无需处理。

    尝试查询缓存,在这里为窗口的四个角构建一份遮罩对象并缓存,使用 OpenGL 将遮罩和着色器进行关联,激活两个材质分别绘制窗口内容和四个角的遮罩,在着色器中完成窗口圆角的半透明效果。

    限于篇幅,本文不展开介绍如何实现圆角插件的全部实现过程,仅挑选关键步骤。

    安装

    将动态库复制到 /usr/share/kwin/effects/plugins ,并使用 DBus 激活插件。

    1
    qdbus --literal org.kde.KWin /Effects org.kde.kwin.Effects.loadEffect scissor

    Thursday, June 1, 2023

    在 V23 beta 版本中,DDE 试验性的开启了 Wayland 的支持,允许用户在 Wayland 协议下的桌面工作环境启动 。本篇文章会向大家介绍一下 Wayland 是什么,我们尝试做了什么改变,以及 DDE Wayland 未来会支持哪些新特性。(注:单独提出 Wayland, 通常和 Wayland 合成器、Wayland 服务器、显示服务器被视为同一个内容;X Window System 和 X11 也被视为同一个内容。)

    什么是 Wayland?

    Wayland 是一个通信协议,规定了显示服务器与客户端之间的通信方式,而使用这个协议的显示服务器称为 Wayland Compositor。Wayland 只专注于图形,并希望使用其他库与输入硬件进行通信,以降低自身的复杂度。Wayland 最大的好处也是大家都推崇的原因,那就是 Wayland 在设计上会考虑安全,例如默认不允许窗口获取其他窗口的数据,合成器和窗口管理器的合并也降低了对系统资源的消耗。

    Wayland 与 X Window System 有什么不同?

    Wayland 与 X Window System 的最大不同在于,Wayland 与 X Window System 的最大不同在于,它的窗口管理器和 Wayland Server 在同一个进程,并且客户端能够通过 EGL 以及一些 Wayland 特定的 EGL 扩充组件直接在显示内存中绘制自己的缓冲区。 窗口管理器简化成显示管理服务,专门负责绘制那些屏幕上的程序。 这比 X Window System 中的窗口管理器要更简单、高效。

    Wayland 协议有哪些组成?

    1.协议概述

    Wayland 协议被描述为异步面向对象协议。协议是异步的,这意味着不必等待回复或者响应 ACK,避免了往返时间并提高性能。协议封装为面向对象的设计,则是面向对象的设计方式,能很好的对服务器上不同窗口数据及接口进行封装。

    Wayland 合成器可以定义和公开自己的附加接口,被称为扩展协议,不同的 Wayland 会提供功能完全不同,甚至功能相反的协议,这带来了很大灵活性,但使用客户端时需要自行判断。

    2.协议架构

    Wayland 协议是一种“客户端 —— 服务器”模型,客户端是请求在屏幕上显示画面的图形应用程序,服务器是控制应用程序显示在屏幕上的管理程序。Wayland 参考实现被设计成两层协议,既:

    • 下层协议:处理客户端和服务器之间的进程间通信,以及在内部的数据封装处理。
    • 上层协议:处理客户端和服务器交换的数据,以实现窗口系统的基本功能,这一层被实现为异步面向对象协议。 下层协议是使用 C 语言开发的,而上层协议是根据 XML 格式的协议描述文件自动生成,每当 XML 协议的描述发生变化时,就可以重新生成该协议的源代码,这使得协议非常灵活、可扩展性好且防止出错。

    如下 Wayland 工作原理图:

    wayland.png

    以下对应图中所标编号作说明:

    Linux 内核中的 evdev 模块接收事件并将它们发送到 Wayland 合成器。

    Wayland 合成器查看场景图并确定哪个窗口应接收事件。场景图对应于屏幕上显示的内容,Wayland 合成器显示对应用事件的场景图中元素的转换。因此,Wayland 合成器可以反向变换以找到正确的窗口,并将屏幕上的坐标转换为窗口中的坐标。

    当客户端收到事件时,Wayland 的客户端只需通过 EGL 渲染并向合成器发送请求以通知更新的范围即可。

    Wayland 合成器 从客户端收集更改请求并重新配置屏幕。 然后,合成器直接发出 ioctl 让 KMS 重绘屏幕。

    当了解了 Wayland 相关基本介绍之后,基于它我们在 DDE 上将会作哪些适配功过呢?将从以下几个方面说一下我们在 Wayland 技术预览版里面所做的适配工作。

    DDE 适配 Wayland 都做了哪些工作

    DDE 原本设计为在 X11 协议下工作,很多组件直接或间接依赖 X11 的接口,多数组件依赖的功能并没有在 XWayland 中提供,所以就需要进行一些修改。

    首先在 Qt 插件中实现和 Wayland 特定性相关的功能,Qt 提供了一个 Wayland Shell Integration 的插件,允许我们在这里调用 DDE Wayland 合成器提供的扩展协议。

    Qt 已提供核心协议的适配,所以 DDE 只需要在现有框架下实现扩展协议即可。

    目前 DDE 提供的扩展协议有以下几个方面:

    • 设置圆角窗口
    • 请求获取窗口数据(截图权限)
    • 划分工作区可用区域 DTK 程序可以通过设置窗口的属性,或者使用 DTK 提供的平台接口,即可调用扩展的 Wayland 协议,非 DTK 程序则需要手动使用扩展协议的 XML 文件进行代码生成调用。

    DDE Wayland 未来会支持什么特性?

    1.HDR 支持

    高动态范围(HDR)是比平时更高的动态范围。HDR 的内容过于深入,在这里只简单的进行说明。

    HDR 可以保存更多的内容信息,在支持 HDR 的屏幕上观看 HDR 的内容,可以获得更好的体验,DDE 目前正在准备支持 HDR 内容的输出,这会让 DDE 拥有更好的显示效果。

    2.成体系的窗口动画

    在 X11 下,由于窗口管理和画面合成管理是两个进程,并且在启动速度上存在差异,所以只能采用一些“巧妙”的设计来规避视觉错误。

    但是在 Wayland 下,窗口管理器和窗口合成器被合并成一个进程,那么启动后就可以立即使用动画效果,例如可以设计视觉效果更好的登录动画。

    目前 DDE 的窗口动画支持并不多,且大部分是单调的线性动画,有些情况还需要客户端自己实现虚假动画,例如任务栏发生位置变化时,桌面的图标会进行计算,并自行改变大小,这引入了非必要的依赖。如果使用窗口动画,桌面并不需要关心任何外部因素,只需要设置自己在可用工作区域最大化,当任务栏发生位置改变时,合成器会自动调整桌面的大小,并产生相应的窗口动画。

    未来 DDE 会使用更多窗口动画来减少组件之间的依赖,以及实现更多更好的视觉效果。

    最后总结

    阅读至此,是不是对 Wayland 的概念及工作原理更加清晰啦?如果您有什么疑问也欢迎与我们互动探讨 Wayland。

    坦白说,DDE 的 Wayland 支持还处于初步阶段(技术预览版,请谨慎使用),未来我们会使用更多合成器提供的功能,来为桌面环境降低开发难度,提升性能。提供更好的体验。

    Tuesday, May 30, 2023

    队员:复旦大学 朱元依、沈扬、朱俊杰
    指导老师:张亮、陈辰
    企业导师:王子冲

    本项目为2023年操作系统大赛企业赛道赛题。

    项目链接:DDE 自启动管理插件 github仓库

    一、目标描述

    我们小组的选题为proj223-control-center-startup-management-plugin

    DDE 深度桌面环境的控制中心提供了插件功能,以便第三方开发者可以扩展其功能并额外的功能组件添加到控制中心之中。我们将在本项目中为 DDE 桌面环境的控制中心编写一个自启动管理插件。

    二、比赛题目分析和相关资料调研

    为了明确题目设置的原因与需求,我们请教了了企业导师,得知:目前用户对自启动权限的管理目前只能通过 dde-launcher (启动器/“开始菜单”)的右键菜单进行管理;考虑到此功能本身也并不复杂,所以设置了此课题,旨在解决这样的问题。

    而对于此开发任务的具体实施,主要需要对dde-dock插件运行的原理和deepin开机自启动的设置方式进行调研,以便进行开发。调研的具体内容包括:

    了解了Qt 插件标准(Qt 插件标准

    阅读整理了dde-dock官方仓库中的插件工作原理(插件工作原理

    学习其他开发者的插件项目(CMDU_DDE_DOCK

    阅读整理了各个论坛中关于deepin开机自启动项的讨论(169824124825726

    三、系统框架设计

    1、项目整体结构设计

    符合dde-dock提供的接口与插件开发规范,本项目将分为插件类(SelfStartPlugin)、部件类(MainWidget)和自启动管理窗口(AppletWidget)分别进行功能的实现。其中,项目的结构和各对象所包含的数据结构与方法如下图所示:

    类图

    2、类功能说明

    插件类SelfStartPlugin负责实现插件与dde-dock交互所必须的接口;部件类MainWidget负责在dde-dock中展示该插件的图标;自启动管理窗口AppletWidget负责实现核心功能:读取Deepin系统的开机自启动项、同时对各个软件的开机自启动进行管理(包括添加、删除、启用、禁用)。

    3、实现描述

    (1)插件类

    在插件类中,主要实现了dde-dockPluginItemInterface相关的接口,便于系统加载并实现插件的功能。接口包括以下内容:

    名称功能
    SelfStarupPlugin类的初始化函数
    pluginName返回插件名称,用于在 dde-dock 内部管理插件时使用
    init插件初始化入口函数,参数 proxyInter 可认为是主程序的进程
    pluginDisplayName返回插件名称,用于在界面上显示
    itemWidget返回插件主控件,用于显示在 dde-dock 面板上
    itemPopupApplet返回鼠标左键点击插件主控件后弹出的控件
    flags用于返回插件的属性,例如插件显示的位置,插件占几列,插件是否支持拖动等
    pluginIsAllowDisable返回插件是否允许被禁用(默认不允许被禁用)
    pluginIsDisable返回插件当前是否处于被禁用状态
    pluginStateSwitched当插件的禁用状态被用户改变时此接口被调用
    itemContextMenu返回鼠标左键点击插件主控件后要执行的命令数据
    invokedMenuItem返回鼠标右键点击插件主控件后要显示的菜单数据

    (2)部件类

    名称功能
    MainWidgetMainWidget部件的初始化函数,设置dde-dock图标的基础样式与文字内容
    ~MainWidgetMainWidget部件的析构函数
    sizeHint设置图标大小的函数
    插件图标

    图标

    最左侧的SELF_STARTUP图标为该插件图标。

    (3)自启动管理窗口

    名称功能
    AppletWidgetAppletWidget的初始化函数,负责绘制与用户交互的开机自启动项展示表格与修改设置选项
    ~AppletWidgetAppletWidget部件的析构函数
    searchAll搜索所有可设置开机自启动的软件
    update更新被自启动的软件
    readfiles工具函数,用于读取文件中的内容
    disable禁用开机自启动设置的后端接口
    enable启用开机自启动设置的后端接口
    add添加开机自启动设置的后端接口
    delete删除开机自启动设置的后端接口
    getFileName通过绝对路径找到文件的名字
    getAllFiles递归获取到某目录中所有文件
    globalSearch在所有文件中找到.desktop类型文件
    Manual寻找拟添加软件的函数,给用户在系统中寻找想要添加自启动项的软件
    showAppsDebug工具,在Debug信息中打印软件名称
    showPathsDebug工具,在Debug信息中打印路径名称
    onButtonClicked自启动管理窗口启用/禁用按钮的处理函数
    addButtonClicked自启动管理窗口添加按钮的处理函数
    delButtonClicked自启动管理窗口删除按钮的处理函数
    自启动管理窗口的前端页面

    AppletWidget的构造函数中,我们实现了便于用户查看系统开机自启动设置信息列表、并便于管理的前端展示页面:

    前端界面

    开机自启动管理的后端接口

    该部件中,主要设置了四个函数接口实现与操作系统的交互,分别是searchAll、update、disable、enable

    首先要在MainWidget启动的时候在/home中读到用户名username

    searchAll()

    opt/apps中找到所有用户下载程序的文件夹,读入文件夹名称subdir,再到opts/appp/subdir/entries/application中找到.desktop启动文件,解析文件内容,读入name字段并把name - path存到MainWidge类中,并且返回所有找到的app名称

    update()

    根据启动时读到的username在/data/home/username/.config/autostart里面找到里面所有的.desktop文件,分别读取并获取状态Hidden是否为false,把所有Hidden字段为False的插件(被设置成了开机自启动)在MainWidget的selfSetUp成员中设置为pair<name, true>

    disable()

    功能设计为禁用自启动。通过name_path中对name的索引找到路径path,对设置为自启动的应用path会在/data/home/username/.config/autostart/*.desktop中。读取该.desktop文件修改Hidden字段为True即可

    enable()

    功能设计为启用自启动。对于之前添加过的应用,name_path中得到的路径是在/data/home/username/.config/autostart中,此时与disable()过程相同,把Hidden字段设置成false即可;对于之前没有添加过的应用,name_path中的路径会在opts/appp/subdir/entries/application/appname/entries/application/*.desktop中,并且该文件是只读的。需要读取该文件并写入到/data/home/username/.config/autostart/*.desktop中,并且在文件的第一个分区里面写入一行"Hidden=false"

    Add()

    用户点击后打开文件资源管理器对话框,从中选择希望添加的可执行文件并返回文件路径。如果该文件是一个.desktop类型文件则添加到autostart中,如果不是则会在autostart中创建一个新的.desktop文件,并且把Exec=行设置为该可执行文件的路径

    Delete()

    从管理窗口中删除某应用的管理。传入参数是应用名称,找到该应用的.desktop文件路径并从autostart中删除,并且在数据结构name_pathselfSetUp中删去该部分信息

    四、开发计划

    第一步(4/9~4/18)

    • 调研Deepindde-dockQT框架等相关内容
    • 设计项目方案
    • 分工

    第二步(4/19~5/2)

    • 搭建主体插件类的框架
    • 设计启动项管理窗口的前端展示页面

    第三步(5/3~5/13)

    • 开发部件类接口
    • 完善插件类功能

    第四步(5/14~5/21)

    • 插件类右键功能开发
    • 完成配置文件

    第五步(5/22~5/31)

    • Debug
    • 撰写文档

    五、比赛过程中的重要进展

    日期进展
    4/17完成开发环境配置
    4/27完成插件框架设计
    5/3完成自启动管理界面的实现
    5/8完成自启动管理接口实现的讨论
    5/12完成自启动操作项的接口设计与实现
    5/21完成配置文件,并通过cmake编译
    5/22完成图标的Debug,前端界面展示正常
    5/29完成自启动操作接口Debug

    六、系统测试情况

    1、前端测试

    在项目总路径下运行install.sh脚本,可完成插件的编译与安装:

    sh install.sh
    

    该脚本使用cmake编译代码,并安装到dde-dock插件文件夹中。运行后,可以正常展示自启动管理页面。点击加号后,用户可以在弹窗中选择想要添加自启动项的软件。

    结果

    右键功能展示正常:

    右键

    2、自启动管理功能测试

    以浏览器为例。根据自启动管理窗口的设置,我们尝试添加并启用浏览器的自启动项。关机重启后,浏览器完成自启动。同理,删除、禁用功能均通过测试。

    七、遇到的主要问题和解决方法

    1、开发环境配置

    (1)、配置 Deepin 操作系统

    开发环境:Deepin 20Beta版

    系统架构:x86

    镜像下载链接:Deepin 操作系统下载链接

    虚拟机平台:WMware Workstation 16Pro

    操作系统环境搭建参考博客:环境搭建博客

    (2)、配置 Deepin 插件开发环境

    安装基本开发环境:

    安装包 build-essentialgitg++cmakeddedtk

    sudo apt install build-essential git g++ cmake
    sudo apt install dde-dock-dev libdtkwidget-dev
    
    安装 QT 开发环境:

    安装 qt5-defaultqt5-docqtcreator

    sudo apt install qt5-default qt5-doc qtcreator
    

    依照上述方法,可在虚拟机中运行qtcreator,并在qtcreator中对插件进行测试

    image-20230513155011159

    (3)、插件安装测试

    为了测试所配置的虚拟机环境可用于 DDE 插件的开发,在环境配置中,本小组选取了Github仓库中的插件dde-sys-monitor-plugin(项目地址:dde-sys-monitor-plugin插件仓库)进行试运行。

    根据上述方法配置插件开发环境后,可按照dde-sys-monitor-plugin中的提示信息顺利运行该插件。这表明开发环境配置已完成。

    (4)环境配置中遇到的问题

    安装sudo apt install libdtkwidget-dev libdtkgui-dev libdtkcore-dev出错

    正试图覆盖 /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/dtk_install_dconfig.prf,它同时被包含于软件包 libdtkcommon-dev 5.5.23-1

    在处理时有错误发生:

    /var/cache/apt/archives/libdtkcore-dev_5.6.4-1+rb1_amd64.deb
    E: Sub-process /usr/bin/dpkg returned an error code (1)
    

    解决方法:

    使用sudo apttitude install libdtkwidget-dev libdtkgui-dev libdtkcore-dev 选择第二种解决方式(先卸载nY再重新安装sudo apt install libdtkwidget-dev libdtkgui-dev libdtkcore-dev)

    2、开机启动项处理思路

    为了实现插件可视化管理软件的开机自启动,了解Deepin系统开机自启动的功能实现是至关重要的。

    通过查阅资料与实践,我们了解到Deepin系统包含自启动文件夹~/.config/autostart,该文件夹类似于 Windows 下的启动文件夹,系统开机时会执行该文件夹下的每个 desktop 文件 Exec 参数指向的脚本或可执行文件。

    为了确认可行性,小组进行了该方法的验证。首先,通过Deepin系统自带的修改开机自启动设置的方法,修改开机启动项(图中修改终端的自启动项):

    deepin自启动修改

    随后检查自启动文件夹~/.config/autostart

    yang@yang-PC:~/.config/autostart$ ls
    deepin-terminal.desktop org.deepin.browser.desktop
    

    发现deepin-terminal.desktop文件被添加入了该自启动文件夹中,并且会在开机时自启动。

    而取消终端的自启动时,~/.config/autostart中已有的deepin-terminal.desktop文件并不会被移除,而是其中的Hidden的字段会被修改为false,表示取消开机自启动设置。

    由此,可以通过在插件中检查所有~/.config/autostart文件夹中.desktop文件的Hidden字段来搜索系统所有的自启动软件;也可以通过添加.desktop文件、修改Hidden字段的方式进行开机自启动设置的修改。

    3、与 deepin 接口设计

    (1)、关于开机自启动项

    在查找deepin如何实现开机自启动时,看到了在deepin文件夹中有多个名为autostart的文件夹,其中有一部分包含了系统文件(例如终端、输入法等),但是在我们通过“开始”菜单修改其是否自启动性质时发现并为出现变化,并且我们自行在“应用商店”里面下载的软件并没有在设置为开机自启动后进入到该文件夹。经过在网络上搜索了关于deepin开发的一些讨论资料以及学习了一些关于启动文件.desktop文件功能并通过vscode对是否开机自启动设置后的文件进行比较后,找到了文件路径位于/data/home/username/.config/autostart中,并且在文件中有Hidden字段使得不用每次取消自启动是都要删除文件并且下次设置又重新加入

    (2)、关于查找应用程序

    一开始的想法是在系统目录下整体查找,找到系统中所有的.desktop文件,但显然这样过于繁琐且效率低下。后来在通过打开每个引用的.desktop文件后发现exec项均在/opt/apps中,打开该文件夹可以发现里面有所有用户下载的应用程序及其依赖等文件的文件夹,并在打开每个文件夹后可以找到启动文件均在entries/application中,并且可能有多个.desktop文件,所以把自启动的工作变为了对启动器文件用文本方式打开后的解析与修改

    (3)、关于函数接口设计

    对QT库中的数据类型和class的使用不太熟悉,需要根据官方文档对提供的api和C++模式的代码进行替换。但熟悉使用之后理解到了QT数据类型的多样性以及提供的方法会更加完备,在做开发时避免了很多自己重写方法的复杂流程。

    4、cmake中遇到的问题

    (1)、cmake 出现 dbus、core 找不到

    解决方法:find_package()的REQUIRED后面新增找不到的对应的包

    (2)、cmake 出现 FOUND=FALSE

      /usr/lib/x86_64-linux-gnu/cmake/DtkWidget/DtkWidgetConfig.cmake                                                                                                                   
      but it set DtkWidget_FOUND to FALSE so package "DtkWidget" is considered to                                 
      be NOT FOUND. 
    

    解决方法:使用.pro文件设置cmake需要的相关参数。

    八、分工和协作

    朱元依:插件类框架开发、部件类前端开发

    沈扬:自启动管理功能逻辑设计、插件类右键功能开发

    朱俊杰:自启动管理窗口后端接口开发(添加、删除、启用、禁用)

    九、提交仓库目录和文件描述

    .
    ├── CMakeLists.txt
    ├── README.md
    ├── aboutdialog.cpp    #关于窗口的实现文件
    ├── aboutdialog.h     #关于窗口的头文件
    ├── aboutdialog.ui     #关于窗口的UI文件
    ├── appletwidget.cpp   #自启动管理窗口的实现文件
    ├── appletwidget.h    #自启动管理窗口的头文件
    ├── images        #图片
    │ ├── QT_IDE.png
    │ ├── QT_前端.png
    │ ├── deepin自启动修改.png
    │ ├── 类图.jpg
    │ ├── 右键.png
    │ ├── 图标.png
    │ ├── 结果.png
    │ ├── 中期类图.jpg
    │ ├── 中期测试.png
    │ └── 前端界面.png
    ├── install.sh        #插件安装脚本
    ├── main_aboutdialog_test.cpp #关于窗口的测试文件
    ├── main_test.cpp      #测试文件
    ├── mainwidget.cpp     #插件类的实现文件
    ├── mainwidget.h      #插件类的头文件
    ├── self_startup.json     #插件的元数据文件,指明了当前插件所使用的 dde-dock 的接口版本
    ├── self_startup.pro     #辅助 cmake 的配置文件
    ├── self_startup.qrc     #用于展示插件图片
    ├── selfstartupplugin.cpp   #部件类的实现文件
    ├── selfstartupplugin.h    #部件类的头文件
    ├── uninstall.sh        #插件卸载脚本
    ├── 初赛报告.md
    └── 过程文档.md

    十、比赛收获

    借由为deepindde-dock编写插件的机会,我们小组了解了deepin系统相关的接口、dde-dock插件加载原理、开发逻辑等等操作系统相关的知识;同时我们在合作开发的过程中熟悉了软件工程的开发规范。小组同学在比赛中均受益匪浅。

    十一、与企业导师的沟通情况

    我们已于企业导师(王子冲)通过电子邮件进行联系。王导师向我们推荐了 deepin 开源社区各种公开渠道(如实时聊天渠道 Matrix、开发者社区讨论板等),鼓励我们在开发过程中将所遇到的问题在社区研发话题板块中进行公开探索。

    此外,王导师还耐心的向我们介绍了该课题的设置原因,这对与我们在项目设计的过程中了解用户需求起到了很大的作用。

    Monday, May 29, 2023

    【摘要】arm64架构支持 v23仓库已经支持arm64和amd64架构软件包,arm64架构的基础环境已经具备,现在就差镜像制作工具的支持了,镜像构建工具的目标是构建出标准pc镜像。为此我借来一台紫光 飞腾D2000机器进行arm64的适配工作,这台机器有相对标准的UEFI固件,目前已经支持UEFI安装, 阅读全文

    Monday, April 10, 2023

    目前 dde-nixos 已经分叉,mian 分支进行 v23 的维护,目前主要更新了 dtk 和部分 deepin 开头的应用, dde 开头的核心应用移植暂未实现,dbus 接口不兼容,因此目前不可日常使用。

    gomod 分支用于测试使用 buildGoModule 完成构建,仅验证可行性,实际使用还需要调整硬编码相关的 patch。

    日常使用 DDE 需要切换 v20 分支,会优先使用已经提交到上游的应用:

       dde-nixos = {
          url = "github:linuxdeepin/dde-nixos/v20";
          inputs.nixpkgs.follows = "nixpkgs";
        };
    

    在 v20 分支,dtk 使用 5.6.3 不再升级,deepin 应用会保持最新的 v20 版的最新版本(不会上 6.0.0),dde 应用冻结为 1 月份打包时测试可用的版本,一般不再升级:

    既除了 deepin 应用,其他应用只有在 v23 移植完成后再更新。

    目前 NixOS 23.05 — Feature Freeze & Release Blockers 已经开始,进度请关注: https://github.com/NixOS/nixpkgs/issues/224457#issuecomment-1501383113

    向上游贡献的主要调整:

    1. 调整 patch

    在 dde-nixos 中,编写了 getPatchFrom, replaceAll 等函数帮助 patch 硬编码路径,但打包时为上游添加函数是难以接受的,因此所有的 patch 都需要使用 substituteInPlace 重写:

    一个典型的例子是:https://github.com/NixOS/nixpkgs/pull/217806

    1. 改善对交叉编译

    所有 deepin 启用 strictDeps,调整了 nativeBuildInputs 和 buildInputs 不规范的地方,(使用 qmake 的除外,会造成 qtwebengine 找不到,且上游已经不太关心 qmake)

    strictDeps 下无法传播 qtimageformats 问题 ,由 NickCao 解决

    ps:可以使用 nix-build -A pkgsCross.riscv64.deepin.dtkcore 尝试交叉编译。目前 x86_64 和 aarch64 可正常编译。

    Friday, April 7, 2023

    obs 全称 Open Build Service,是一个开放的构建平台。相较于其他构建工具有以下优点:

    • 支持跨平台构建(x86、arm64 等)
    • 支持多种虚拟环境(kvm、lxc、chroot 等)
    • 支持软件包构建(deb、rpm、pkg 等)
    • 支持容器构建(flatpak、appimage、docker 等)
    • 支持发型版镜像构建(debian、windows 等)

    玲珑 是一种新型的独立包管理工具集,致力于治理 Linux 系统下传统软件包格式复杂、交叉的依赖关系导致的各种兼容性问题,以及过于松散的权限管控导致的安全风险。

    本文介绍怎么给 obs 添加玲珑构建支持,供以参考实现你自己的 obs 构建服务。

    Thursday, March 9, 2023

    deepin v23 beta 的发布在即,为了能够使相关的 bug 能够得以更快解决,并促进研发团队的协作变得更高效,我们(开源社区中心)决定在 deepin 员工内部举办小规模的 bug hunting 性质的比赛,并命名该比赛为“小浣熊杯修 bug 大赛”。而首届“小浣熊杯”也于昨天顺利落幕,那么就让我们一起了解一下这个比赛吧!

    比赛介绍

    如上所述,“小浣熊杯“的比赛大致内容即为在比赛时间内对已有缺陷进行修复。我们将所有参赛的员工划分为多个组,每个组除研发外也配备一个测试人员。在比赛时间周期内,研发从指定的缺陷看板中挑选自己”中意“的 BUG 进行修复,并在修复后将修复公布在相关群内,由 其他组 的测试人员进行测试。当修复被测试人员验证没有问题后,即可进行计分。最终,会以本组研发人员所修复的数量与测试人员所完成的测试数量相组合,并计算小组人均得分,最后以小组人均得分的高低决定最终排名。

    小组得分的计算公示为:

    小组得分 = (本组研发修复的缺陷数量 + 本组测试所验证的缺陷数量 / 3) / 小组总人数
    

    比赛会在 https://github.com/linuxdeepin/.bug-game/ 中进行,每次比赛会创建一个看板来跟进整个比赛的实时情况,并创建一个对应的 issue 记录相关进展与结果。

    另外,考虑到比赛过程中对缺陷的修复不需要经过其他研发人员的 code review ,因而可能存在实际的代码质量问题,故相关的 PR 均不要求在比赛结束前合入,相关提交仍需按照正常流程,经过有效 review 获得 approval 后合入。

    首届状况介绍

    比赛过程看板截图

    首届比赛共划分了四个小队参赛,比赛时间从 3 月 7 日开始,为期两天。比赛过程与结果在 这个 Issue 中汇总,最终的缺陷修复情况也可以参见 这个看板。比赛过程中“修 bug”队一度领先,随后被”进击的小浣熊“队反超,最终经过了两天的”激烈比拼“后,本次比赛总计处理了 42 个 Issue,由”进击的小浣熊“队以 18.333 分的总积分获得人均积分第一夺冠。“修 bug”队紧随其后,“呆呆鹅” 与 “bug 收割小分队” 获得随后的名次。

    比赛过程中,各个小组对已有 bug 的挑选与“占坑”以及测试人员对新提交修复的“抢单”是过程中最有趣的事情之一。快速挑选便于修复的 BUG 并进行有效的修复成为了获胜的关键之一,根据表象快速分析推测问题的能力,以及在陌生项目1中快速尝试定位和修复问题的能力也变得至关重要。作为花絮,有的小组也在选择 BUG 的过程中连续发现自己所选择的缺陷实际早已在版本迭代中被修复,耗费了较多时间而造成了相对的失利,但这个过程也对现存 BUG 的有效性验证有很大的帮助2

    赛后,我们进行了比赛的颁奖,获胜队伍获得了比赛限定奖杯与荣誉证书,以及一个 deepin 主题背包。获奖队伍也进行了合影:

    荣誉证书

    颁奖截图

    无论是否获奖,我们都感谢各个参赛队伍的积极参与,也希望各位能在后续的比赛中能够获得优异的成绩。


    1. 注:缺陷的修复不限于自己所维护的项目,研发人员也可以尝试修复由其他项目组所维护的缺陷。 ↩︎

    2. 或许在后续的比赛中应当为此类也算作计分项。 ↩︎