ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

终结者源码解析:从Linux终端分屏到GTK插件定制

终结者源码解析:从Linux终端分屏到GTK插件定制 简介终结者源码是一份远程访问工具RAT的源代码面向网络安全爱好者、逆向分析人员及具备一定编程基础的学习者旨在通过真实项目剖析RAT的设计与实现。源码覆盖远程桌面查看、程序运行、系统设置管理、命令控制与反馈等核心模块并涉及TCP/IP、HTTP/HTTPS、SSL/TLS等通信与加密机制同时包含多平台兼容处理、编译调试、代码混淆、自我加密等隐蔽性技术以及持久化后门和权限提升的实现细节。资源包为rar压缩格式大小约3MB目前已有690人学习下载。深入阅读这份源码可以直观理解恶意软件规避检测的常见手段提升对网络攻击链的认知也可以借鉴其中的网络编程、事件驱动与模块化设计思路用于安全实验或防御方案研究。前提是遵守法律法规仅将其用于正当的安全研究与教学场景。1. 终结者源码不是电影道具它是每日都在用的 Linux 终端分屏利器如果你在终端里工作超过两年一定经历过开满桌面窗口的焦虑日志、汇编、SSH、编译任务各占一块切换全靠鼠标。终结者源码要解决的就是这个场景——它对应的是 Linux 上最常见的分屏终端模拟器 Terminator把一个终端窗口拆成任意网格每个格子里跑独立的 shell。很多人误以为“终结者”是什么安全工具或电影项目其实它是一段可以自由修改的 Python/GTK 程序。读这份源码你能看到终端模拟器如何与 Linux 伪终端交互、如何把 VTE 控件塞进网格布局还能在不改上游的情况下给自己加一个“一键同步输入”的按钮。适合正在学 Python、GTK 或想做办公环境定制的开发者也适合不想被键盘绑定限制的深度用户。这篇笔记从拉取源码讲起把依赖、启动、布局实现、常用踩坑和插件扩展一次说清。2. 先把源码拉下来跑通环境依赖与最小启动命令2.1 源码结构这 6 个模块决定了 Terminator 的启动路径终结者源码的布局并不复杂核心是一个名为terminatorlib的 Python 包。顶层目录里的bin/terminator是入口脚本它负责初始化多语言、配置和单例 Terminator 对象真正干活的模块都在terminatorlib下。我通常拿到源码包后先打开六个关键文件terminator.py主窗口与进程循环、window.pyGTK 窗口封装、terminal.pyVTE 终端控件封装、container.py布局容器基类、paned.py分栏实现、layout.py布局保存/还原。这六个文件串起来就覆盖了一条自启动到渲染的完整调用链。文件职责读源码时重点看bin/terminator程序入口处理参数与单例参数如何传给 Terminatorterminatorlib/terminator.py主对象负责窗口生命周期create_window流程terminatorlib/window.py封装一个 GTK 顶层窗口信号槽如何绑定terminatorlib/terminal.py包裹 VTE 控件子进程、退出码处理terminatorlib/paned.py水平/垂直分割逻辑分割时如何接管焦点terminatorlib/layout.py布局树保存与还原递归遍历容器为什么从这几个文件开始因为实际改动最频繁的就是布局和终端行为。比如你想让新终端默认进入某个目录改的是terminal.py里的spawn参数你想重做分屏快捷键改的是preferences.py里注册的 keybinding。不了解模块边界找一天也找不到地方下刀。源码里还有一个factory.py负责按配置创建终端和容器建议把它当作第七个文件一起读它能把 “配置字符串” 到 “GTK 对象” 的转换过程讲清楚。如果你用的是 IDE建议先在项目根目录跑一次grep -rn class .*: terminatorlib | head -30把类清单拉出来。类名本身就是一张迷你架构图Terminator、Window、Terminal、Container、Paned、Notebook、Plugin。这些类之间没有复杂的继承链多数是组合关系——Terminator持有Window列表Window持有根ContainerContainer递归挂载Terminal或子Container。理解这种树形组合读分屏逻辑会轻松很多。2.2 从 Git 克隆到本地运行一条命令启动也踩坑拿到源码不一定非要先安装。Terminator 设计上允许直接以源码方式运行这给开发调试省了很多打包时间。常见做法是先克隆上游仓库然后执行入口脚本# 从你 fork 或上游克隆仓库名以官方发布为准 git clone https://github.com/gnome-terminator/terminator.git cd terminator # 直接运行不写入 system 目录 python3 bin/terminator这段命令的逻辑是python3会读取bin/terminator第一行的#!/usr/bin/env python3指定的解释器而import terminatorlib需要当前目录在sys.path里。在仓库根目录运行时Python 会自动把工作目录加入导入路径所以很少遇到 ModuleNotFoundError。但如果你从别的目录执行python3 /path/to/terminator/bin/terminator大概率会报找不到terminatorlib原因就是根目录没有进入搜索路径。这时用PYTHONPATH/path/to/terminator python3 /path/to/terminator/bin/terminator能救回来。要提醒的是在无显示器的服务器上直接执行python3 bin/terminator会卡在Gtk3初始化或直接抛Gtk-WARNING **: cannot open display。这不是你环境坏了而是 Terminator 本质是一个需要图形会话的 GTK 程序。要测启动至少要有 X11 或 Wayland无人值守时用xvfb-run包一层再跑我放最后一章演示。另外源码版本尽量选 tag 而不是 master 分支因为 master 可能正在切 GTK4依赖写法和配置文件结构都变了。git tag --sort-v:refname | head能看到最近发布版挑一个最新稳定 tag 检出来能少踩一半的坑。2.3 依赖安装GTK3、VTE 与 Python 包的版本暗坑终结者源码跑起来的依赖比一般终端工具要多一层它直接调 GTK3 的 Python 绑定和 VTE 的 GObject 绑定。在 Debian/Ubuntu 系上我通常一次性装齐sudo apt install \ python3-gi \ python3-gi-cairo \ gir1.2-gtk-3.0 \ gir1.2-vte-2.91 \ gir1.2-glib-2.0 \ python3-configobj \ python3-psutil \ python3-dbus \ intltool \ gettext装完之后python3 -c import gi; gi.require_version(Vte,2.91); from gi.repository import Vte; print(ok)能输出来才算稳。这里最容易出问题的是 VTE 版本源码里有注释明确写着使用Vte 2.91这是对应 GTK3 的 API 版本如果你系统里只有gir1.2-vte-2.90import 时会直接抛ValueError: Namespace Vte is not available。另一个坑是python3-psutil它负责收藏进程树和退出告警缺了之后 Terminator 能起来但关闭最后一个窗口时会报异常。所以不要自作主张精简依赖那等于给自己埋雷。在 Fedora 系上对应的包名是python3-gobject、gtk3、vte291和python3-psutil在 Arch 上则是gtk3、vte3、python-gobject。跨发行版之前先查 VTE 主版本确认是 2.91 而不是 2.90。我的习惯是为 Terminator 单独做虚拟环境或容器时只用发行版包管理器装系统级 GTK 依赖Python 依赖由pip install configobj psutil requests补这样能把“系统库”和“纯 Python 库”混血带来的问题隔离在固定环境里。依赖锁定这件事对二次开发很重要很多“我按教程跑不起来”的提问最后都是因为发行版默认装的是旧 GTK 或 Python2 时代的包。3. 读源码的入口从 main 到 window一次源码剖析与架构实战3.1 入口文件与对象模型很多阅读者对着源码找main找不到因为bin/terminator不是以if __name__ __main__结尾的而是直接放了一个main()。入口脚本做的事情很简单处理命令行参数、初始化 gettext、创建 Terminator 单例、调用主循环。摘出关键骨架后长这样# 从 bin/terminator 简化只保留核心启动路径 from terminatorlib import i18n, config, factory from terminatorlib.terminator import Terminator def main(): i18n.setup_gettext() cfg config.Config() term Terminator() term.create_window(factory.APP_NAME) if term.process(): term.main() if __name__ __main__: main()这里config.Config()是配置文件的唯一工厂内部用 ConfigObj 读~/.config/terminator/configTerminator对象不是 GTK 窗口而是程序生命周期的管理者它维护一个窗口列表。create_window会依据布局配置递归创建容器最终生成一个 Gtk.Window。term.main()实际调用的是Gtk.main()进入事件循环后所有 UI 行为都由 GTK 回调驱动。理解这个对象模型后面改插件的思路就顺了你要挂接的每个新功能本质都是往 GTK 信号链路上加一个 handler。如果你在 Ubuntu 20.04 上跑的是 GTK3.24还可以在create_window前后各放一行print观察窗口对象创建前后的引用计数变化。这会让你意识到GTK 窗口不是 Python 对象销毁就立刻消失而是要在Gtk.main()退出信号里才释放底层 C 对象。这也是很多人 Linux API 看到一半犯迷糊的地方Python 的 GC 和 GTK 的 ref-count 是两套系统普通代码不用管但写插件时如果持有不该有的引用窗口关闭后崩溃就离你不远了。3.2 分屏布局的实现逻辑Terminator 的分屏看起来是“切一刀”内部却是一棵多叉树。根节点是一个 Paned 容器子节点可以是另一个 Paned也可以是 Terminal。每个 Paned 保存方向horizontal/vertical和两个子容器的位置比例。源码中的paned.py负责分裂动作当你在终端里按 SuperO 做水平分割时它把当前 Terminal 替换成一个新的 Paned再把旧 Terminal 和新 Terminal 作为左右孩子挂进去。配置文件里的布局树直接展示了这一结构以默认布局为例[layouts] [[default]] [[[child0]]] type Paned parent direction horizontal position 500 [[[child1]]] type Terminal parent child0 [[[child2]]] type Terminal parent child0这段配置的意思是顶层容器child0是水平 Paned左半child1是终端右半child2是终端。position 500指分隔条初始像素位置。布局还原时layout.py从根节点开始深度优先遍历遇到 Paned 就调用paned.py建分割遇到 Terminal 就交给factory构造终端。这也是源码里最容易读的一环因为递归逻辑很直接。如果你想做“记住当前分屏布局并一键恢复”只要复制这一段 INI 到~/.config/terminator/config即可不用改一行 Python。但要注意运行时创建的新分割并不会自动写回配置文件。paned.py里的split方法是纯内存操作它只修改容器树然后让 GTK 重新布局。如果你在分屏后立刻看配置文件发现里面没有新增节点这是正常行为。只有当你调用保存布局的功能时layout.py才会把内存中的树序列化成 INI。这个设计有个好处配置文件的布局树始终是“初始状态”不会被手滑分屏污染坏处是刚改完源码或插件时很容易误以为“布局没保存”是 bug。3.3 配置加载与布局持久化配置文件是 Terminator 功能最集中的地方。config.py用 ConfigObj 读取 INI 语法提供了get_global,get_layout,save_layout等接口。save_layout在关闭窗口时把当前容器树写回配置这就是“布局持久化”的机制。很多人以为布局文件是自动生成的实际它只在“首选项里点击保存布局”或崩溃信号触发时才写盘。手动改配置时要注意三点配置项大小写敏感type必须严格等于Paned/Terminalparent引用的是兄弟节点 ID不能写成0position是相对偏移单位是像素而非百分比。写错后 Terminator 启动时会弹出“加载布局失败”的对话框但不会阻止窗口打开它会退回默认单终端布局。这种退化行为设计得很宽容适合边改边试但也容易让人忽略语法错误。我的排查习惯是把出错的配置行单独摘到一个文件里用python3 -c from terminatorlib import config; config.Config().load(bad.ini)看完整异常堆栈比盯着 GUI 对话框猜快得多。布局持久化另一个容易被忽略的点是宽高比例。position在配置文件里写的是绝对值但如果你的屏幕分辨率变了这个值可能让分隔条跑到屏幕外。Terminator 的应对策略是在启动时用gtk_paned_set_position传入配置值如果 GTK 计算出的分配宽度小于该值会强制修正到可用范围。读源码时看到position min(position, allocation - min_size)类似的表达式不要觉得多余它解决的就是跨屏幕迁移的边界问题。4. 常见问题从源码运行和定制 Terminator 的 5 个典型踩坑记录4.1 现象克隆后直接python3 bin/terminator报ModuleNotFoundError原因运行目录不是仓库根目录terminatorlib不在sys.path或者克隆下来的源码里有未编译的.c扩展没有就绪。解决回到仓库根目录执行不行就显式设PYTHONPATH。我验证过PYTHONPATH/opt/terminator /opt/terminator/bin/terminator在多数 shell 里都能跑通但 zsh 在源码目录之外设环境变量时要注意路径名空格。实际更隐蔽的情况是系统里同时存在 Python 2.7 的terminator包老发行版自带。python3不会导入 Python2 的包但如果你在旧 Ubuntu 上同时装了新旧两套 GTK 绑定import gi可能会抓到旧命名空间里的Gtk。检查办法是python3 -c import gi; gi.require_version(Gtk,3.0); from gi.repository import Gtk; print(Gtk)输出应该指向/usr/lib/python3/dist-packages/gi。如果路径带dist-packages/gi但 import 却报了版本缺失多半是gi的缓存.pyc文件损坏删掉源码目录__pycache__再跑。4.2 现象启动后窗口内是一片黑/灰终端不显示 prompt原因VTE 库版本不匹配或者 Terminator 调用gdk_threads_init方式与 GTK3 冲突。常见于gir1.2-vte-2.91与gir1.2-vte-2.90两包共存。解决清掉旧依赖只保留 2.91。Ubuntu 上可用apt-cache search gir1.2-vte查看当前源里有哪些版本如果源里只有 2.90说明你的发行版过老应该先升级 GTK3 到 3.24 以上再编译 VTE 的 GObject 绑定。不要尝试手改gi.require_version因为 VTE 2.90 和 2.91 的 API 在控件构造参数上完全不同。还有一个我见过多次的玄学窗口是灰色但鼠标变成了输入光标说明终端在等待 shell 启动。这通常是SHELL环境变量指向了不存在的路径Terminator 按配置文件里的command执行 shell 时 fork 失败。解决方法是先跑echo $SHELL确认/bin/bash存在再试试在配置文件里显式写command /bin/bash。这个坑和源码无关但源码阅读者最容易甩锅给 Terminator。4.3 现象插件加载报GLib-GIO-WARNING或者自定义插件被忽略原因Terminator 的插件目录有优先级。源码仓库里的terminatorlib/plugins/是内置插件用户插件在~/.config/terminator/plugins/。两个位置放了同名.py文件时用户目录覆盖内置目录但不会报错。解决确认你改的文件在用户目录检查插件类是否继承了Plugin基类并且register方法签名正确。注意插件文件名必须以.py结尾且文件内不能有顶层print之外的副作用代码否则会被importlib的执行机制污染命名空间。新写插件时最容易漏的不是语法而是缺少capabilities列表。比如你要自定义右键菜单就必须明确声明capabilities [context_menu]想拦截标题变更就得声明capabilities [title_hint]。没声明能力时Terminator 仍然会 import 这个模块但不会把终端实例传给你接口自然没有任何反应。调试时可以先用terminator -l列出当前加载的插件如果列表里没有你新增的插件优先查文件名和后缀是否被忽略。4.4 现象分屏快捷键按了无反应但其他快捷键正常原因在 Wayland 会话下GTK 的某些键组合被桌面环境先行截获比如 SuperH 可能被 GNOME 用作隐藏窗口。Terminator 的 keybinding 是在应用内注册的桌面环境不会把按键事件传给应用。解决换一个无冲突的组合比如把水平分割从SuperO改成CtrlShiftO。修改点在preferences.py中的default_keybindings字典或者运行时通过首选项对话框改。我在 GNOME Wayland 上实测过SuperO有时会被左上角活动视图吃掉CtrlShiftO则稳定。这里还要注意 X11 和 Wayland 下键盘事件的差异。X11 下 GTK 能拿到大部分修饰键组合Wayland 的协议里则允许合成器先处理全局快捷键。另一个容易踩的是CtrlAltT这种系统级终端默认键它会被桌面抢先绑定为打开系统终端。我在改 keybinding 时习惯先用xev或wev检测按键是否真正到达应用检测到之后再改配置能省掉反复重启页面的时间。4.5 现象每次启动都弹 “last child closed” 错误日志或者退出时 core dump原因Terminator 主循环在最后一个终端窗口关闭时事件循环没有完全退出导致 GTK 对象在底层被销毁后仍持有引用。新版源码在window.py的delete_event里处理了GTK 3.24的Gtk.main_quit但如果你在插件里添加了长期运行的GLib.idle_add回调回调会在窗口销毁后再触发引发段错误。解决在插件on_window_destroy信号里取消所有 idle/超时回调并调用GLib.source_remove。排查时用gdb抓 backtrace十有八九路径指向你最近注册的 GLib 回调。这是定制后崩溃最多的一类原因一定要养成清理回调的习惯。更隐蔽的触发点是 DBus 服务。Terminator 单例模式会占用一个总线名如果上一次崩溃没有释放新进程启动时会在 DBus 注册阶段等 5 秒超时。日志里出现DBusException: name already owned时先pkill terminator再启动或者等系统自动清理残留。这个坑多数人在写自动重连插件时会碰到属于“启动顺利退出崩溃重启卡住”三连环处理完回调再把 DBus 连接按on_close断开就好了。5. 把源码改成你的终端Keybinding 与自定义插件5.1 修改 Keybinding 的最小补丁如果你只是想改默认分屏键不需要懂整个架构。源码里preferences.py有一份default_keybindings字典键名是功能键值是按键描述。拿水平分割来说默认值是Supero改成CtrlShiftO只动一行# terminatorlib/preferences.py 中局部修改示例 default_keybindings { split_horiz: CtrlShifto, # 原来是 Supero split_vert: CtrlShifte, close_terminal: CtrlShiftq, # 其余保持不变 }这段代码的逻辑是Terminator 在启动时读取这个字典将按键描述交给 Gtk.AccelMap 解析最终绑定到对应回调上。需要注意的坑有两个一是按键描述里每个修饰键必须用Ctrl这种尖括号写法且大小写敏感二是如果该按键已被其他全局快捷键占用Gtk 不会报错只是事件传不到。改完后重启 Terminator 看效果不需要重新编译因为 Python 是解释执行的。如果你想同时保留两套快捷键可以临时把新组合加在字典里但不用删旧的。不过这样会在preferences.ui的列表里显示重复条目容易让用户误解。我更推荐的做法是直接改字典并改为用户级的 keybinding 覆盖在~/.config/terminator/config的[keybindings]段里写split_horiz CtrlShifto配置优先级高于源码默认值升级源码也不冲突。这个技巧很多人不知道它能让你在不碰源码的情况下完成自定义是项目里最省心的“后悔药”方案。5.2 写一个简单的 Plugin在标题栏显示 Git 分支自定义插件是 Terminator 源码扩展里最活跃的入口。插件类是terminatorlib.plugin.Plugin的子类需要实现register方法返回可被主循环调用的对象。我写过一个小插件作用是在终端标题里附加当前 Git 分支名抄作业可以直接看这个骨架# file: ~/.config/terminator/plugins/gitbranch.py import os import subprocess from terminatorlib.plugin import Plugin from terminatorlib import terminator class GitBranch(Plugin): capabilities [title_hint] def register(self, term): self.term term term.connect(title-change, self.on_title_change) def on_title_change(self, terminal, title): branch self._current_branch(terminal.get_cwd()) if branch: new_title f{title} [{branch}] terminal.set_title(new_title) return True # 阻止旧标题覆盖 staticmethod def _current_branch(path): if not path: return None try: out subprocess.check_output( [git, -C, path, branch, --show-current], stderrsubprocess.DEVNULL).decode().strip() except Exception: return None return out or None这段插件的逻辑是插件启动时把自身实例附加到Terminator终端标题变化时回调on_title_change用git -C 目录 branch --show-current拿当前分支名拼在旧标题后面。注意register方法只会在终端已经真实创建后触发所以不能在里面用Gtk.main_iteration。签名title_hint是 Terminator 2.9 之后才有的能力旧版源码不自带属于插件 API 演进的结果。return True是告诉 GTK 停止继续传播这个信号避免别处的 handler 再改一次标题。如果你在测试中发现标题不刷新先确认两件事插件文件是否被扫描到用terminator -l列出插件时是不是多了GitBranch终端所在目录真的是 git 仓库。比起改内置代码插件方式的好处是升级源码时不冲突坏处是插件 API 在大版本之间会变读一读对应版本的plugins/README.md比撞文档靠谱。插件里调set_title实际上走的是 VTE 标题变更事件它会触发窗口标题更新所以不要在回调里再次读标题避免死循环。5.3 改动后的验证与回归手工测试清单改完源码或插件我一般不会立刻投产先跑一个小清单。这个清单也是我评审团队 PR 时常用的改动内容验证动作通过标准keybinding按新组合拆分窗口两次分割出现三个终端焦点正确插件在 git 仓库打开新终端标题栏出现[main]后缀布局保存分屏后保存布局重启后恢复到相同几何位置退出关闭最后一个窗口进程退出无 segfault兼容远程 SSH 到另一台机器终端内 shell 环境变量不受污染手工清单虽然不自动但能最快暴露“改 Python 动态绑定导致全局信号异常”的问题。每次改完代码重启 Terminator 前我会先python3 -m py_compile一下改动过的文件语法错误会在进入 GUI 前暴露。如果改动的是 keybinding我还会用python3 -c from terminatorlib import preferences; print(preferences.default_keybindings)确认字典没有被前面某个导入模块意外覆盖。6. 验证与进阶用 Xvfb 做冒烟测试把回归关进笼子里Terminator 这种 GUI 工具在 CI 里跑自动化总让人头疼。常见做法是用 Xvfb 虚拟出一块显示器然后在上面启动 Terminator用xdotool模拟按键最后看进程退出码。我验证源码改动时会把这个流程压进一个 40 行脚本每次提交前跑一遍。#! /usr/bin/env bash set -euo pipefail # 用 Xvfb 分配 1280x800 虚拟屏锁住 99 号 Xvfb :99 -screen 0 1280x800x24 XVFB_PID$! trap kill $XVFB_PID EXIT export DISPLAY:99 # 启动修改过的 Terminator给 5 秒完成初始化 python3 ./bin/terminator TERMINATOR_PID$! sleep 5 # 用 xdotool 找到终结者窗口并按下水平分割 WINDOW_ID$(xdotool search --name Terminator | head -1) xdotool windowfocus --sync $WINDOW_ID key ctrlshifto sleep 1 # 如果分割成功窗口标题会增加进程仍在运行 kill -0 $TERMINATOR_PID echo smoke-ok kill $TERMINATOR_PID || true wait $TERMINATOR_PID 2/dev/null || true这个脚本的思路很直接先用 Xvfb 提供 X 协议环境再以源码方式启动 Terminator接着用 xdotool 转发按键最后检查进程活着。它最大的价值不是替代真机测试而是把“启动→快捷键→退出”这条最容易翻车的路径锁死。我第一次给一个内部 PR 加插件时就靠它抓出GLib.idle_add导致的退出崩溃没有虚拟屏的话桌面会话会被直接带崩简直血泪。进阶用法我建议在跑通冒烟后做三件事第一用strace -f -e traceprocess看 Terminator 启动时分离出几个子进程理解 VTE 终端与 shell 的父子关系第二在bin/terminator里临时插一条import pdb; pdb.set_trace()用调试器在create_window处停住看布局树在第一次分屏前长什么样第三把改过的 keybinding 提交到自己的 fork之后每次上游升级就用git rebase拉新源码看你的改动有没有冲突。这三件事做完你基本就具备独立维护 Terminator 分支的能力了。回过头看终结者源码并不是一个需要膜拜的黑匣子它更像一个用 Python 包着 GTK 的活标本读得懂入口和容器树就掌握了一半的定制权避开依赖版本和 GLib 回调两个大坑剩下就是自己的肌肉记忆。我的习惯是每次改完源码都先跑一遍 Xvfb 脚本再回桌面免得屏幕闪一下还找不到原因。希望帮到你。本文还有配套的精品资源点击获取
返回列表