后台Terminal(命令行)是本地IDE的常用功能,对项目git操作和文件操作有非常强大的支持。对于WebIDE来说,在没有web伪终端的情况下,仅仅提供一个封装好的命令行接口是不够开发者使用的。因此,为了更好的用户体验,开发网页伪终端也提上了日程。关于终端(tty)和伪终端(pty)的区别,可以参考Whatdoptyandttymean?调查一下终端,它类似于我们理解中的命令行工具。通俗地说,就是可以执行shell的进程。每次在命令行输入一系列命令并回车,终端进程都会fork一个子进程来执行输入的命令。终端进程通过系统调用wait4()监听子进程的退出,同时通过暴露的stdout输出子进程执行信息。如果在web端实现类似本地化的终端功能,还需要做更多的工作:网络延迟和可靠性保证,shell用户体验尽可能接近本地化,web端UI宽高和输出信息适配,安全访问控制以及权限管理等。在实现web端之前,需要评估哪些功能是最核心的。明确:shell的功能实现,用户体验,安全(web端是线上服务器提供的功能,所以安全性是必须要保证的)。只有在保证这两个功能的前提下,网页伪终端才能正式上线。我们先来看这两个功能的技术实现(服务端技术采用nodejs):nodenative模块提供了一个repl模块,可以用来实现交互输入输出,并提供tab补全功能和自定义输出样式等功能,但是它只能执行node相关的命令,所以不能达到执行系统shell的目的。node原生模块child_porcess提供了spawn,一个uv_spawn函数,封装了底层libuv,底层执行系统调用fork和execvp。执行外壳命令。但是,它不提供伪终端的其他特性,如tab自动补全、方向键显示历史命令等操作。所以在server端使用node的native模块是不可能实现一个伪终端的。需要继续探索伪终端和节点端的原理。实现方向。伪终端伪终端不是真正的终端,而是内核提供的“服务”。终端服务通常包括三层:顶层提供字符设备的输入输出接口,中间层线路规程(linediscipline),底层硬件驱动,其中顶层接口往往通过系统调用函数实现,例如(读,写);底层硬件驱动负责伪终端的主从设备通信,由内核提供;linediscipline看似比较抽象,但实际上它在功能上负责输入输出信息的“处理”,比如处理输入过程中的中断字符(ctrl+c)和一些退格字符(backspace和delete),等,并将输出的换行符n转换为rn等。一个伪终端分为主设备和从设备两部分,它们的底层通过实现默认线路规程的双向管道(硬件驱动)相连。伪终端主机的任何输入都会反映在从机上,反之亦然。从设备的输出信息也通过管道发送给主设备,这样shell就可以在伪终端的从设备中执行,完成终端的功能。伪终端的slave设备可以真实模拟终端的tab补全等特殊的shell命令。所以,在nativenode模块不能满足需求的前提下,我们需要从底层看OS提供了哪些功能。.目前glibc库提供了posix_openpt接口,但是过程有些繁琐:使用posix_openpt打开一个伪终端主设备grantpt设置从设备的权限unlockpt解锁对应的从设备获取从设备名称(类似对/dev/pts/123)主(从)设备进行读写,执行操作。因此,出现了一个封装更好的pty库,仅通过一个forkpty函数就可以实现以上所有功能。通过编写节点C++扩展模块并使用pty库实现一个终端,该终端在伪终端上从设备执行命令行。关于伪终端的安全性,我们会在文末讨论。伪终端实现思路根据伪终端主从设备的特点,我们在master设备所在的父进程中管理伪终端的生命周期和资源,在子进程中执行shell从设备所在的进程,执行过程中的信息和信息结果通过双向管道传输给主设备,主设备所在的进程对外提供stdout。在此处借用pty.js的现实思路:pid_tpid=pty_forkpty(&master,name,NULL,&winp);switch(pid){case-1:returnNan::ThrowError("forkpty(3)failed.");案例0:if(strlen(cwd))chdir(cwd);if(uid!=-1&&gid!=-1){if(setgid(gid)==-1){perror("setgid(2)失败。");_退出(1);}if(setuid(uid)==-1){perror("setuid(2)失败。");_退出(1);}}pty_execvpe(argv[0],argv,env);perror("execvp(3)失败。");_退出(1);默认值:if(pty_nonblock(master)==-1){returnNan::ThrowError("Couldnotsetmasterfdtononblocking.");}Local