阅读视图

发现新文章,点击刷新页面。

GTK应用开发小记

✇BeaCox
作者 BeaCox

夏季学期课程的小组作业,是要开发一个基于Linux内核模块的包过滤防火墙。主要有两部分的任务:

  1. 配置程序

    运行在应用层,用来配置过滤规则,包括协议类型、IP地址、端口号、开始和结束时间、是否启用规则等。

  2. Linux内核模块

    运行在内核层,完成包过滤防火墙的功能,该模块借助注册Netfilter钩子函数的方式来实现对数据包的过滤和控制。

我主要负责了第一部分的任务:开发一个友好的包过滤规则的配置和管理界面(GUI部分,CLI部分由组里另一位同学负责)。支持包过滤的规则导入、导出,添加、编辑、 删除、搜索等功能。应用界面如下:

日间模式
暗黑模式
编辑页面
关于页面+日志页面

谈不上好看,但也不至于很丑。

GTK vs QT

GTK和QT是非常有名的两个GUI库,当然QT应该是更有名些。GTK和QT的优势对比如下:

  • QT:
    1. 跨平台性:QT是一个跨平台的工具包,可以在多个操作系统上运行,包括Windows、Linux、macOS等。它提供了一致的API,使得开发者可以轻松地编写一次代码,然后在不同的平台上进行部署和运行。
    2. 高度集成:QT提供了丰富的组件和工具,涵盖了广泛的应用开发需求,包括图形渲染、网络通信、数据库访问等。它还提供了开发者友好的IDE和调试工具,使得开发过程更加高效。
    3. QML和Qt Quick:QT引入了QML和Qt Quick技术,允许开发者使用声明性语言和组件化的方式来设计和构建用户界面。这种方式简化了UI设计和开发的过程,并提供了良好的可扩展性。
    4. 商业支持:QT由The Qt Company开发和维护,提供了商业许可和支持服务。这对于企业级应用开发来说是一个优势,因为他们可以获得专业的技术支持和保障。
  • GTK:
    1. 开源性:GTK是一个开源工具包,它的代码可以被自由地查看、修改和分发。这对于开源社区和个人开发者来说是一个优势,他们可以根据自己的需求进行自定义和改进。
    2. UNIX哲学:GTK是基于UNIX哲学设计的,它鼓励模块化和简洁的设计。这种设计理念使得GTK在Linux等UNIX-like系统上有着很好的集成和兼容性。
    3. GNOME集成:GTK是GNOME桌面环境的默认工具包,它与GNOME的集成非常紧密。如果你计划开发适用于GNOME桌面环境的应用程序,使用GTK可能更加方便和自然。
    4. 多语言支持:GTK支持多种编程语言,包括C、C++、Python等。这使得开发者可以使用自己喜欢的编程语言来进行应用程序的开发。

最终我是选择了GTK3进行GUI开发,原因如下:

  1. 这次开发的防火墙程序是基于Linux内核模块的,所以只能在Linux系统使用,不需要考虑GUI的跨平台。
  2. Glade应用提供了GTK应用的UI设计功能,起到和QML、Qt Quick类似的作用。
  3. 开源、不需要商业支持。
  4. 防火墙属于网络层的应用,不需要太多功能,简洁至上。
  5. 在Ubuntu22.04环境下开发,使用GTK接近原生UI。
  6. GTK的默认样式足够好看。

GTK & glade学习

GTK相比QT的一个最大劣势就是文档更少、社区也更不活跃。B站和YouTube搜索QT,有非常多的教程,而GTK相对来说就比较少了。另外GTK4已经问世数年,但是教程大多还是GTK3。之前提到用来设计GTK应用UI的glade,支持的最高GTK版本也是GTK3。

好在对于这样一个简单的GUI应用,只需要入门GTK便可。学习一样工具,我总是喜欢边学边做。因此视频+文档的组合往往是更适合我的。在我学习GTK开发的过程中,主要参考了以下资源:

  1. Linux Gtk Glade Programming

    YouTube上的GTK & glade开发教程,没有涵盖GTK的所有类,但对入门来说够用而且友好。

  2. 视频中的源代码

    更多时候我其实是直接看源代码学习,视频节奏有些拖沓,一旦理解GTK和glade是怎样工作的,看代码会是更高效的解决方法。

  3. GTK3文档

    文档很全面,但只有英文。

  4. ChatGPT

    文档没写全的、视频没讲到的可以问问GPT。看看思路可以,3.5写出来的代码可能不能直接用。

GTK & glade开发流程

使用GTK & glade开发,主要是应用UI设计和功能实现分离的思想。

  1. 在glade应用中设计UI

    哪里是按钮,哪里需要输入框,哪里需要列表等等,需要提前构思好。

  2. 在glade应用中连接信号(signals)

    所谓信号,就是当用户与界面发生某种特定的交互时,应用程序便会知悉,并可根据这种信号回调对应的函数、传入特定的数据进行特定的操作。可以在glade中连接信号并指定对应的回调函数,以及需要传入的数据。这样在后续功能实现时,只需将这些函数的功能实现即可,也很好地实现了模块化。

  3. 编写GTK代码

    主要是实现之前在glade中指定的回调函数。另外,一些用于提示用户的对话框也可以直接用代码生成。

  4. 编译程序

    在开发阶段,一般从glade文件加载builder(gtk_builder_new_from_file),并使用gcc-export-dynamic参数。这样一来,修改glade文件后无需重新编译就可以看到新的UI。

    而在生产环境中,不能使用上述方法。因为上述方法编译的应用程序需要依赖glade文件运行,而一般用于生产环境的应用程序需要将glade文件一同编译成最后的二进制程序。因此要从资源中加载builder(gtk_builder_new_from_resource)。因此首先要把glade文件编译成资源,这个过程需要用到glib-compile-resources工具。具体方法可以参照Linux Gtk Glade Programming Part 34: Embedding resources in your app

难点

  1. TreeView

    GTK中的TreeView以及ListBox是非常重要的组件,适合用于用户与系统的数据交互,区别在于TreeView可以有多层父子结构,而ListBox只有单层。

  2. Log功能

    Log功能的第一版思想是:每隔一段时间(如1s)监测日志文件的变化,当日志文件大小发生改变时,将新增的内容显示在应用的TextView当中。但是如果使用一个线程,会导致应用要轮流处理与用户的交互和日志文件的监测,而日志文件又需要频繁监测,造成较差的用户体验。因此为监测日志文件变化的功能单独创建一个线程进行处理。

    但是线程需要应对一系列互斥与共享的问题,因此我换了一种实现方法。

    第二版的思想是:使用GFileMonitor来监测文件的变化,当文件变化时,会发出一个信号,GTK应用能捕捉这个信号并做出相应的处理。

    GFileMonitor VS 多线程:

    • 优点:

      • GFileMonitor使用更简单,不需要自己编写多线程逻辑。它提供了文件变化事件的回调接口,只需要关心事件处理逻辑。
      • GFileMonitor对文件系统事件的处理可能更高效。它基于操作系统提供的文件变化监控机制,不需要频繁地轮询检查文件。
      • GFileMonitor可以方便地跨平台使用,而自行实现的多线程文件监视可能需要针对不同平台调整。
    • 缺点:

      • GFileMonitor的可定制性较低,不能自由控制轮询频率等参数。
      • GFileMonitor可能不支持监视网络文件系统或一些特殊文件系统。
      • GFileMonitor基于系统调用,系统开销可能略大于纯用户态的多线程实现。
      • 自行实现的多线程方案可以加入更多自定义逻辑,例如合并事件、缓存等。

源代码(完整程序)

TINY Scanner开发文档

✇BeaCox
作者 BeaCox

前言

NIS2336编译原理课程的大(?)作业。

代码仓库:

要求如下:

前期准备

了解TINY language语言,并能用TINY language写较简单的程序;
掌握词法分析的步骤方法,能根据程序段模拟自动机的分析过程生成token序列。

TINY language词法分析功能说明

TINY language语言的词法单元:

词法单元

文件global.h中定义了所有的词法单元类型TokenType,并在lexer.h中声明。本次实验要求在读懂lexer.c中已有代码的基础上完善补全lexer.c中的主函数getToken(void),该函数通过判断当前状态并根据当前读入的词法单元来输出当前读入词法单元的token,并更新状态和词法单元,根据给出代码中的示例补全switch语句中case为其他状态时的情况。

处理结果要求

给定一段符合TINY language语法的代码,写成.tny文件,放在build\test文件夹内。要求程序能够输出这段代码的每一行,在每一行的后面输出这一行所有词法单元的token。

示例输入和输出如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
read x

if 0 < x then

fac := 1

repeat

fact := fact * x

x := x – 1 until x = 0

write fac

end
example1

example2

提交要求和方法

本次实验只对lexer.c进行修改,其他文件不进行修改。在提交时只需将修改完善后的lexer.c上传即可。

DFA

DFA

开始状态为START,终止状态为DONE
START状态转移到下一个状态,只需要判定下一个读入的字符即可。

ERROR

从图中可以看到,没有将词法错误ERROR单独作为一个状态来参与状态转移。接下来我们考虑发生ERROR的两种情况:

  1. 读入的第一个字符不是{、数字、字母、:以及TINY允许的运算符,则当前字符发生ERROR
  2. 读入的第一个字符是:,但是第二个字符不是=,则上一个字符(:)发生ERROR

Code

1.

1
2
3
4
5
6
7
8
9
10
11
else
{
switch (c)
{
...
default:
state = DONE;
currentToken = ERROR;
break;
}
}

save此时为默认值true,将当前读入的(错误)字符保存以备输出

2.

1
2
3
4
5
6
7
8
9
10
case INASSIGN:
state = DONE;
if (c == '=')
currentToken = ASSIGN;
else
{ /* backup in the input */
ungetNextChar();
save = FALSE;
currentToken = ERROR;
}

若进入INASSIGN后(即读入:后),读入的字符不是=,发生了错误。
调用ungetNextChar()回退一个字符,令该字符参与下一轮的扫描。
save置为false,因为发生错误的是前一个字符:,而不是当前读入的字符。

Lexeme

从示例图的预期输出以及util.c中的printToken()函数可知:

当词法单元类型为Reserved Words, ID, NUMERROR时,需要输出其对应的词素。因此在编写程序时,当遇到这几种词法单元,需要将读入的字符逐个保存,以便在到达DONE状态时输出。

Code

1
2
/* lexeme of identifier or reserved word */
char tokenString[MAXTOKENLEN + 1];

定义了一个名为tokenString的字符数组,用来保存上述情况下的词素。

1
int tokenStringIndex = 0;

每一轮扫描会将字符数组的索引置零,以便保存新的词素。

1
2
3
4
5
6
if (state == DONE)
{
tokenString[tokenStringIndex] = '\0';
if (currentToken == ID)
currentToken = reservedLookup(tokenString);
}

每一轮扫描结束在字符数组结尾加上终止符\0,因为本轮保存的字符串长度可能小于上一轮保存的字符串长度,这样做可以避免上一轮保存的字符串影响这一轮的输出。

1
2
/* flag to indicate save to tokenString */
int save;

定义了一个整型变量save(实际当作布尔型用),用来指示当前读入的字符是否需要保存到tokenString
在每一轮循环的开始,即每读入一个新的字符后,save都被置为true,默认要保存该字符。

1
2
if ((save) && (tokenStringIndex <= MAXTOKENLEN))
tokenString[tokenStringIndex++] = (char)c;

每一轮循环的尾部,如果savetrue,将当前读入的字符保存到tokenString尾部。

实际上,只有进入INIDINNUMINASSIGN或读入的字符非预期时,才需要保存读入的一串字符。下面我们逐个情况讨论:

  1. 进入INID

    1
    2
    else if (isalpha(c))
    state = INID;

    START转移到INID时,仅仅转移状态,save仍为默认值true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    case INID:
    if (!isalpha(c))
    { /* backup in the input */
    ungetNextChar();
    save = FALSE;
    state = DONE;
    currentToken = ID;
    }
    break;

    进入INID后,当且仅当读入非字母时扫描结束。
    此时调用ungetNextChar()函数回退一个字符,令该字符参与下一轮的扫描。
    save置为false表示当前读入的字符不是该轮扫描所得词素的一部分。

  2. 进入INNUM

    实现方法与情况1一致

  3. 在前文讨论ERROR时已给出

  4. 在前文讨论ERROR时已给出

Reserved Words

Reserved WordsID的区别在于:前者由TINY语言预定义,后者由程序员自定义。因此,不严谨地说,Reserved Words也是一种ID。这样,我们可以很自然地将Reserved WordsID的扫描合并。

具体来说,当读入第一个字符为字母的时候,进入INID状态。在遇到非字母字符时才能转移到DONE状态,在这之前我们都无法确认读入的字符串(即词素)是否是Reserved Words。因此在INID状态期间我们将二者一视同仁,而在转移到DONE状态后对二者进行区分。换句话说,判断读入的词素是否是Reserved Words

前文说到,当词法单元类型为Reserved WordsID(亦即进入INID状态)时,我们需要将读入的词素保存。我们在一个包含所有Reserved Words键值对的表进行查找匹配,若保存的词素与表中的词素相同,则返回相应的词法单元,否则表明该词素对应的词法单元为ID

Code

1
2
3
4
5
6
/* lookup table of reserved words */
static struct
{
char *str;
TokenType tok;
} reservedWords[MAXRESERVED] = {{"if", IF}, {"then", THEN}, {"else", ELSE}, {"end", END}, {"repeat", REPEAT}, {"until", UNTIL}, {"read", READ}, {"write", WRITE}};

定义了一个关键字的字典。

1
2
3
4
5
6
if (state == DONE)
{
tokenString[tokenStringIndex] = '\0';
if (currentToken == ID)
currentToken = reservedLookup(tokenString);
}

当一轮扫描结束时,如果读入的词素被判定为ID(即该轮扫描经过了INIDDONE的状态转移),则在关键字字典中查找该轮保存的词素。如果查找到,返回相应的词法单元类型;如果未找到,返回ID

基于GitHub Actions的看雪论坛自动签到,可选推送与否

✇BeaCox
作者 BeaCox

看雪论坛称得上是国内较好的安全论坛了。不过要1k雪币(论坛虚拟币,新用户几乎都可以获得220及以上)才可以升级为正式会员。临时会员有诸多限制,包括不能查看『WEB安全』版块等。对于我这种想白嫖的安全小白来说,唯一的方法就是每天签到随机获得1-10枚雪币。但是我经常会忘记签到,这等到猴年马月?
正好我最近正在学习JS,于是写了一个自动签到的脚本。当然,除了升级正式会员,雪币还有许多用处,所以对已经是正式会员的用户来说也还算有些用罢。

先上传送门:

实现方法

这个脚本的实现非常简单。

  1. 通过抓包可以发现,看雪论坛的签到是通过向https://bbs.pediy.com/user-signin.htm页面发送含Cookie的POST请求来实现的(也是绝大多数签到业务的设计逻辑),因此利用Axios库的API来向该页面发送请求,模拟用户签到。
  2. 签到完成后,将响应的数据赋值给一个对象,通过response.data.coderesponse.data.message来判断网络正常情况下,签到任务的三种可能情况。
    • code == 0 && message = <签到获得雪币数> : 表示签到成功。推送消息显示`签到成功,获得${msg}雪币`。
    • code == -1 && message == '您今日已签到成功' : 表示已经签到过,此处为重复签到。推送消息显示’您今日已签到成功’。
    • code == -1 && message == '请先登录': 表示Cookie验证失败。打印错误并不推送消息
  3. 推送消息的功能利用pushplus提供的接口实现,因为比Server酱免费版限制少一些,当然后续可能会添加server酱等其他选项。同样是利用了Axios的库来向接口发送请求。可以参考pushplus文档中心
  4. 利用GitHub Actions,在GitHub提供的主机上用node运行js,通过crontab完成定时任务。
  5. GitHub Actions在仓库60天以上没有任何活动时会被suspended(推迟),因此利用Keepalive Workflow来使工作流按期运行。

后续

希望各位能帮我点一个star✨(理直气壮)
由于这是我第一个js脚本,程序健壮性想必不甚好,欢迎大家提出issue和pr!

❌