伪终端的从设备中,可以真实的模拟终端的tab补全和其他的shell特殊命令,因此在node原生模块不能满足需求的前提下,我们需要把目光放到底层,看看OS提供了什么功能。目前,glibc库提供了posix_openpt接口,不过流程有些繁琐:
使用posix_openpt打开一个伪终端主设备 grantpt设置从设备的权限 unlockpt解锁对应的从设备获取从设备名称(类似 /dev/pts/123)主(从)设备读写,执行操作
因此出现了封装更好的pty库,仅仅通过一个forkpty函数便可以实现上述所有功能。通过编写一个node的c++扩展模块,搭配pty库实现一个在伪终端从设备执行命令行的terminal。
关于伪终端安全性的问题,我们在文章的最后在进行讨论。
伪终端实现思路
根据伪终端的主从设备的特性,我们在主设备所在的父进程中管理伪终端的生命周期及其资源,在从设备所在的子进程中执行shell,执行过程中的信息及结果通过双向管道传输给主设备,由主设备所在的进程向外提供stdout。
在此处借鉴pty.js的实现思路:
pid_t pid = pty_forkpty(&master, name, NULL, &winp); switch (pid) { case -1: return Nan::ThrowError("forkpty(3) failed."); case 0: if (strlen(cwd)) chdir(cwd); if (uid != -1 && gid != -1) { if (setgid(gid) == -1) { perror("setgid(2) failed."); _exit(1); } if (setuid(uid) == -1) { perror("setuid(2) failed."); _exit(1); } } pty_execvpe(argv[0], argv, env); perror("execvp(3) failed."); _exit(1); default: if (pty_nonblock(master) == -1) { return Nan::ThrowError("Could not set master fd to nonblocking."); } Local<Object> obj = Nan::New<Object>(); Nan::Set(obj, Nan::New<String>("fd").ToLocalChecked(), Nan::New<Number>(master)); Nan::Set(obj, Nan::New<String>("pid").ToLocalChecked(), Nan::New<Number>(pid)); Nan::Set(obj, Nan::New<String>("pty").ToLocalChecked(), Nan::New<String>(name).ToLocalChecked()); pty_baton *baton = new pty_baton(); baton->exit_code = 0; baton->signal_code = 0; baton->cb.Reset(Local<Function>::Cast(info[8])); baton->pid = pid; baton->async.data = baton; uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid); uv_thread_create(&baton->tid, pty_waitpid, static_cast<void*>(baton)); return info.GetReturnValue().Set(obj); }
首先通过pty_forkpty(forkpty的posix实现,兼容 sunOS和 unix等系统)创建主从设备,然后在子进程中设置权限之后(setuid、setgid),执行系统调用pty_execvpe(execvpe的封装),此后主设备的输入信息都会在此得到执行(子进程执行的文件为sh,会侦听stdin);