Flutter 终端模拟器开源篇
前言
- 代码仅供交流学习,所有代码的开源都选择了宽松的协议。所有代码都在大量测试中,请勿随意引入到正式项目中使用。
开源列表
dart_pty
简介
创建一个伪终端并执行一个子进程。也是终端模拟器的底层实现。 与语言提供的执行进程不同,它不仅能够无缓冲的拿到 stdout、stderr 输出,还能拿到所有的终端序列,也能对进程进行交互。
起因
在各种语言都提供了创建子进程的函数,但这类函数都无法无缓冲的获得子进程在运行时得到的输出,并且无法与子进程交互。
例如:
在终端模拟器运行
1 | python |
那么它应该输出
1 | Python 3.8.6 (default, Oct 8 2020, 14:06:32) |
而我们在 java 中使用:
1 | Runtime.getRuntime().exec("python"); |
python 中:
1 | system("python"); |
c 语言:
1 | system("python"); |
dart 中
1 | Process.start("python"); |
进程创建后,我们并不能够与子进程交互,并且无论是在哪一种语言,我们执行python
都获取不到像终端一样的输出,这是由于缓冲还没有满 4096 字节(通常),在进程不主动刷新缓冲的情况下,我们是获取不到它的输出的。
c 语言被编译成二进制后能交互,是因为 system 函数子进程与主进程是同一个输入输出。
原理解析
原理解析移步文章:Flutter 终端模拟器探索篇(二)| 完整终端模拟器
涉及东西有点多,不是本文关注的重点。
开始使用
这是一个纯 dart 项目,你可以集成到 dart 项目或者 flutter 项目中。
配置 yaml
1 | dart_pty: |
已经上传 dart package,在发布 1.0 版本前, dart package 上代码可能差异会很大。
导入包
1 | import 'package:dart_pty/src/unix_pty_c.dart'; |
创建 pty 对象
1 | Map<String, String> environment = {'TEST': 'TEST_VALUE'}; |
当对象创建的时候,就已经存在一个读写指向同一进程的文件描述符了,通过对这个文件描述符的写入即可实现与子进程的交互,读取即可获得子进程的输出,这个输出是无缓冲的。
读写子进程
1 | await Future.delayed(Duration(milliseconds: 100), () async { |
缺点
- 需要引入 so 库。
- 还不支持 windows。
能不用集成 so 库就能实现本地终端吗?
使用 dart:ffi
来编写原来底层的逻辑,这样就能完全的脱离 c 语言部分,不用再为每一个平台编译一份本地库,与西南交大的大佬在 20 年暑假的时候其实就已经实现了,他采用了 isolate
来解决了读文件描述符会阻塞 UI 线程的问题,但isolate
是不能热重载的,并且当终端数量过多的时候,vscode 就会显示一堆isolate runtime
。
而我想要通过设置文件描述符非阻塞的方式来实现,终端的创建与子进程的 fork 都没有问题,但在执行以下代码的时候,发现了差异性。
在 PC 上能够正常的运行,而在 android 设备上失效,跟设备上flag
的宏定义应该有关系,只要能找出O_NONBLOCK
这个宏在安卓上的定义值就能解决。
除了以上问题,dart:ffi 在使用一些头文件的时候,在 android 端存在几率性 crash 的情况。
以下为 dart 代码:
1 | void setNonblock(int fd, {bool verbose = false}) { |
这段 dart 代码原本的 c 语言代码为:
1 | void setNonblock(int fd) |
- unix_pty:ffi 实现。
- term.c:底层实现。
关于 dart:ffi 的使用详见 Dart FFI 探索(三)| 新思路使用 FFI
termare_view
支持全平台的终端模拟器,使用 Flutter 开发,不依赖平台代码。
如果想得到更稳定的 flutter 终端模拟器,可以使用 xterm.dart,上面提到西南交大的大佬开发的,我选择自己开发一份主要是为了更多是适配移动端的表现,还有就是对这个终端花费了很多精力,自己项目的终端组件还是想要通过自己经手编写。
起因
这是在“Flutter 终端模拟器探索篇(三)| 原理解析与集成”文章的探索后,持续开发维护后的代码仓库。
其中因为一些个人的技术原因,导致了一次代码的重构。
无法扩展与持续维护后,我尝试读了xtem.dart
与了解 xterm.js
的一些原理实现,最后选用canvas
作为终端的上层组件。
需求分析
需要维护一个自己的终端模拟器,这个模拟器需要集成到移动应用或者桌面应用,起初考虑用android-terminal-emulator
,后来因为这个终端模拟器已经很久没有再维护过了,就转向了termux
,termux
在整个组织中都有非常丰富的开源,但如果完全使用termux
代码集成到个人项目,在安卓端我可以跳转activity
并通过am
命令发送广播通知原生终端模拟器自动键入命令,但在桌面端就没有办法。
于是idea就出现了,如果用Flutter
重写这个模拟器呢?一次编写,到处运行,想法很美好,但对我来说,的确很难,整个过程比较坎坷,随着技术的不断学习,很多实现也在逐渐向一种规范的方式靠近。
第一想到的是读termux
的源码,注释极少,非常难理解,在移动端自定义源有非常多的坑,于是想着请教一些会知道整个开发流程的人,首先termux
的作者肯定是会的,再就是neoterm
的开发者,我认识他的时候他才高三。就已经上架了neoterm
这个终端模拟器,目前下载量11w,所以在整个开发中他与xterm.dart
的作者都很好的帮到了我。
前面开发的重大错误
- 我不应该将终端模拟器的上层渲染组件与本地底层的
pseudo terminal
强行整合起来,参考了xterm.js
与xterm.dart
后,终端应该是一个独立的上层组件,可以接收任何的输入流才对,除了本地终端的输入流,还可以是来自 ssh 的流,或者其他地方的输入。 - 不应该在未参考现有类似组件实现的情况下盲目编写,选用了上层为
WidgetSpan
这个组件。
重构后
- 使用 canvas 绘制整个终端模拟器。
- 将终端输入流的内容按终端序列解析成二维数组,在
CustomPainter
内部根据二维数组绘制内容与光标。
开始使用
引入项目
这是一个纯 flutter package,所以只需要在 yaml 配置文件的 dependencies 下引入:
1 | termare_view: |
创建终端控制器
1 | TermareController controller = TermareController( |
showBackgroundLine 属性是打开 debug 的格子背景开关。
使用组件
1 | TermareView( |
让终端显示一些东西
1 | controller.write('hello termare_view'); |
运行如下:
你可能会发现,不就是画了一个格子背景,然后绘制了文本而已吗?再执行:
1 | controller.write('\x1B[1;31mhello termare_view\x1B[0m\n'); |
详见 example 。
使用 dart 的 print 打印 ‘\x1B[1;31mhello termare_view\x1B[0m\n’ 也有效果哦。
取自 xterm.js。
termare_pty
这是一个用 pty 实现本地终端的例子。依赖 dart_pty
与termare_view
。
原理解析
通过第一个开源库dart_pty
,去实现一个本地终端,再将终端的输入输出流与termare_view
绑定起来。
创建 pty
1 | unixPtyC = widget.unixPtyC ?? |
目前写得很死,传入的 libPath 就是依赖的 so 库,在
dart_pty
下有 linux、macos 已经编译好的库,在termare_pty
中也有安卓四个架构的so
库。
绑定到终端组件
1 | while (mounted) { |
这段代码应该是比较好理解的。
controller
是TermareController
。
显示
1 | Widget build(BuildContext context) { |
运行截图
好像截图都有点大,
详见示例代码termare_pty.dart。
termare_ssh
这是一个用 ssh 连接服务器终端的例子,也可以连接 localhost 。
原理解析
通过dartssh
这个纯 dart 的package
来获得一个连接到服务器的输出流,并将输入输出绑定到termare_view
。
连接到服务器
1 | void connect() { |
controller
是TermareController
。
显示
1 | Widget build(BuildContext context) { |
运行截图
Android
Linux
Macos
Windows
Ios
详见示例代码termare_ssh.dart。
总结
开源仅为交流学习,交流学习。
读完的朋友已经很不错了,可能会想说“啥呀,怎么一点用也没有”,有这种想法也是正常的,但如果你对终端模拟器有兴趣,欢迎跟我一起贡献这些仓库,随时与我交流,对这些有任何疑问欢迎评论区留言。
Flutter 终端模拟器开源篇