使用node-pty、xtermjs和vue构建web termianl服务

博客分类: node

使用node-pty、xtermjs和vue构建web termianl服务

前言

在每次想登录服务器的时候,就必须打开终端软件,比如 mac 的 iterm2,然后在用 ssh 登录。作为一个爱折腾的前端开发者,打算用 web 技术实现终端。这样就只需要打开一个 url,就可以登录服务器,对服务器进行操作了,而不需要每次打开终端(太麻烦)。

效果图

GitHub Logo

功能

技术栈

如何使用 web 技术实现终端功能呢?经过调研,最常用的技术如下:

大致代码

前端 xterm 封装

首先是对 xterm 添加一些中间件,实现搜索、全屏、改变窗口大小等功能

import { Terminal } from 'xterm';
import 'xterm/dist/xterm.css';
import 'xterm/lib/addons/fullscreen/fullscreen.css';
import '@/assets/css/index.css';

import * as fit from 'xterm/lib/addons/fit/fit';
import * as attach from 'xterm/lib/addons/attach/attach';

import * as fullscreen from 'xterm/lib/addons/fullscreen/fullscreen.js';

import * as search from 'xterm/lib/addons/search/search.js';

Terminal.applyAddon(fit);
Terminal.applyAddon(attach);
Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search);

export default Terminal;

创建 xterm 实例

实现如下功能:

import Terminal from './Xterm';
import io from 'socket.io-client';

const socket = io(window.location.origin + '/terminal', { reconnection: true });

const term = new Terminal();

term.on('resize', size => {
    socket.emit('resize', [size.cols, size.rows]);
});
// 当在前端输入命令时,触发socket的intput事件,并把输入的信息通过socket传到node层
term.on('data', data => {
    socket.emit('input', data);
});

// 把node层的shell信息通过socket传到前端,使用xterm展示出来
socket.on('output', arrayBuffer => {
    term.write(arrayBuffer);
});

socket.on('pid', pid => {
    pane.pid = pid;
});

window.addEventListener('resize', () => {
    term.fit();
});

node 层

const app = require('express')();
const pty = require('node-pty');
const os = require('os');
const userhome = require('user-home');
const server = require('http').createServer(app);
const io = require('socket.io')(server);

// 根据不同环境使用不同的shell类型
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';

// 创建一个terminal的socket命名空间
io.use('terminal').on('connection', socket => {
    let ptyProcess = pty.spawn(shell, ['--login'], {
        name: 'xterm-color',
        cols: 80,
        rows: 24,
        cwd: userhome, // 首次进入系统根目录
        env: process.env
    });

    // 绑定data事件,通过socket的output事件,把shell日志传到前端
    ptyProcess.on('data', data => socket.emit('output', data));

    // socket绑定input事件,用于接收前端发送的命令,并传到ptyProcess中
    socket.on('input', data => ptyProcess.write(data));

    socket.on('resize', size => {
        ptyProcess.resize(size[0], size[1]);
    });

    socket.on('exit', size => {
        ptyProcess.destroy();
    });

    socket.emit('pid', ptyProcess.pid);
});

至此,一个简单的 web terminal 就创建完成了。但是开发中遇到了一些问题,在此记录下

问题

创建分屏时多个 xterm 的 dom 会和 vue 的 Virtual DOM 冲突

在创建分屏时,会出现某个 xterm 实例的 dom 会丢失。这种现象是由 vue 的 Virtual DOM 和 xterm 同时操作 dom 有关。解决方法是在每次创建 xterm 时,检测已有的 xterm 的 dom 是否还存在,没有存在就重新添加到 dom 中。

// 创建xterm实例之后
this.$nextTick(() => {
    term.open(document.getElementById(terminalname)); // terminalname是当前xterm实例的id名
    // container中包含所有的xterm实例
    container.children.forEach(item => {
        let termEle = document.getElementById(item.name);
        // 判断当前dom中元素和xterm实例创建的dom不一致时,重新把xterm的dom添加的dom中
        if (item.term.element != termEle.children[0]) {
            termEle.innerHTML = '';
            termEle.append(item.term.element);
        }
        item.term.fit();
    });
});

如何在分屏时保持和上一个面板同一个根目录?

经过调研,xterm 和 node-pty 都没有提供这样的功能。但是 node-pty 能够获取上一个实例子进程的进程 id,然后根据 liunx 命令,可以由进程 id 获取当前进程的所在的根目录。大致代码如下:

const express = require('express');
var router = express.Router();

const { exec } = require('child_process');
const { promisify } = require('util');

const promiseExec = promisify(exec);

router.get('/cwd', function(req, res) {
    const { pid } = req.query;
    promiseExec(`lsof -a -p ${pid} -d cwd -Fn | tail -1 | sed 's/.//'`).then(newCwd => {
        const cwd = typeof newCwd === 'string' ? newCwd.trim() : newCwd.stdout.trim();
        res.success(cwd);
    });
});

module.exports = router;

如何像 iterm2 那样更换主题?

xterm 也是支持主题的,但是在 npm 官网上居然没有找到 xterm 相关主题包,所以自己做了一个:xterm-theme。它已经支持了 156 个主题,大家可以随意选择。

import { Terminal } from 'xterm';
import { AdventureTime } from 'xterm-theme';

const terminal = new Terminal({
    theme: AdventureTime // xtermTheme.AdventureTime
});

// 或

import xtermTheme from 'xterm-theme';

const terminal = new Terminal({
    theme: xtermTheme.AdventureTime // xtermTheme.AdventureTime
});

总结

上述代码已经放到我的 github 上了:https://github.com/ysk2014/webshell,大家可以 clone 下来玩一下(觉得好的话,跪求 start)。这个可以放在自己的开发机上,提供大家对开发机的使用效率。如果觉得不安全,可以自己在此基础上添加一个登录功能(代码很方便扩展)。