• 爱上开源之DockerUI-xterm.js实现Web控制台


    前言

    xterm.js是模拟终端产品中最为市场推崇的,较多的开源项目在实现Shell模拟终端都是用了这个产品。 在DockerUI中,dockerUI提供了类似Shell的功能,可以在DockerUI里直接连接到容器里,执行容器的终端命令, 类似于在docker环境下,执行docker container exec -it 这样的命令行功能。 在DockerUI里也使用了xterm.js这个项目来实现了WEB方式的模拟控制台终端。

    先看看效果图

    注意看清楚哟, 和Xshell长的很像,但是是WEB方式实现的。

    今天这篇文章,就来谈谈xterm.js在DockerUI里的具体实现Web控制台的过程。

    引入xterm.js

     说实在话,虽然这个xterm.js确实在此类产品中的名气确实最大,但是其官方网站上的文档和资料就真的是匹配不上这个江湖地位了, 文档基本没有, 只能看源码进行猜和试错。 我们DockerUI里对xterm.js的集成,基本上全部都是自己试出来的。

    1. ROOT_RES_URL + "/static/plugins/xterm/lib/xterm.js",
    2. ROOT_RES_URL + "/static/plugins/xterm/lib/xterm-addon-fit.js"
    ROOT_RES_URL + "/static/plugins/xterm/css/xterm.css"

    DockerUI项目没有使用node.js的开发架构, 是使用的CubeUI的前台开发架构,基于EasyUI的前台框架改造而来, 属于类似layui的开发方式,都是原生态的js+html的前后台分离方式。 但是xterm.js的官网里都是基于npm的方式,不适用于这里。

    实例化Terminate对象

    在DockerUI里把这个Terminate创建过程进行封装,封装一个方法来实现

    1. function createTerminate(target, onKey, rows, cols){
    2. rows = rows || 36;
    3. cols = cols || 80;
    4. let term ;
    5. term = new Terminal({
    6. rendererType: "canvas", //渲染类型
    7. convertEol: true, //启用时,光标将设置为下一行的开头
    8. scrollback: 100, //终端中的回滚量
    9. disableStdin: false, //是否应禁用输入。
    10. cursorStyle: "underline", //光标样式
    11. cursorBlink: true, //光标闪烁
    12. cols: cols,
    13. rows: rows,
    14. theme: {
    15. foreground: "#14e264", //字体
    16. background: "#002833", //背景色
    17. cursor: "help", //设置光标
    18. lineHeight: 16
    19. },
    20. bellStyle:'sound',
    21. rightClickSelectsWord:true,
    22. screenReaderMode:true,
    23. allowProposedApi: true,
    24. LogLevel: 'debug',
    25. tabStopWidth: 4,
    26. });
    27. term.onKey((event) => {
    28. if(onKey){
    29. onKey.call(term, event.key, event.domEvent)
    30. }
    31. });
    32. term.open(target);
    33. //term.open(document.getElementById('container-terminal'));
    34. term.writeln('Welcome to web-console of docker.ui');
    35. term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
    36. term.writeln('Type some keys and commands to play around. Press the key "ctrl-Z" to exit the console');
    37. term.focus()
    38. return term
    39. }

    在这一段代码里, 把传入的target对象进行绑定,根据传入的rows,cols产生一个Terminal对象,并且绑定了Terminal的onkey事件,当Terminal对象里有输入时触发事件。如果和后天Docker容器的通信的监听事件绑定到一起,这样Terminal有输入事件,输入字符后,把对应字符转换成命令行,发送到Docker通信的通道里,Docker容器通过通道收到WebConsole发送过来的命令,进行处理,处理后通过通道返回执行结果给Ternimal, Terminal通过调用write或者writeln方法,把返回结果显示到webconsole里即可。  这个方案完全可行。

    开始实现和Docker容器的通信

    很明显这里是个双向通信的通道方式, 通过Web实现双向通信最佳的方案当然就是webSocket的方式了, 所以首先实现go后端的http调用支持websocket。  dockerUI项目的Http服务这块都不是用的http的原生服务,有兴趣的可以关注我的其他的文章,很多文章都谈到了这点, 同样DockerUI的http服务也是使用了fasthttp作为底层服务。  在fasthttp里实现支持websocket的handler。

    1. func InitWsRouter(router *routing.Router, routerGroup *routing.RouteGroup) {
    2. routerGroup.Any("/echo", EchoWSHandler)
    3. routerGroup.Any("/exec", ExecWSHandler)
    4. }
    5. func ExecWSHandler(ctx *routing.Context) error {
    6. execId := string(ctx.FormValue("id"))
    7. if util4go.IsEmpty(execId) {
    8. fasthttputil.Result.Fail("没有设置需要执行的ID").Response(ctx.RequestCtx)
    9. return nil
    10. }
    11. ri, err := getRequestInfo4WS("/docker-api-ws", ctx)
    12. if err != nil {
    13. return err
    14. }
    15. err = upgrader.Upgrade(ctx.RequestCtx, func(ws *websocket.Conn) {
    16. defer ws.Close()
    17. ctx.Request.Header.Del("Origin")
    18. hijackExecStartOperation(ws, ri.host, execId, ri.version)
    19. })
    20. if err != nil {
    21. if _, ok := err.(websocket.HandshakeError); ok {
    22. Logger.Info(err)
    23. }
    24. return err
    25. }
    26. ctx.Response.Header.Set("Sec-WebSocket-Protocol", ri.version)
    27. return nil
    28. }

    前台通过websocket调用此REST API,然后和Terminal交互

    1. let url = context + "/docker-api-ws/exec?id="+response.ExecID;
    2. ws = $.app.websocket(
    3. url,
    4. function(e){
    5. console.log(e);
    6. term = createTerminate(document.getElementById("container-terminal-"+response.ExecID),
    7. function(key, ev){
    8. //ws.send(key)
    9. if(!sendWs(ws, key)){
    10. alert('控制端已经失去通信,请重新打开');
    11. }
    12. });
    13. if(fn){
    14. fn()
    15. }
    16. wses.push(ws);
    17. }, function (e) {
    18. term.write(e.data)
    19. console.log(e);
    20. }, function (e) {
    21. console.log(e);
    22. closeConsoleDg(dgId, false);
    23. }, function (e) {
    24. console.log(e);
    25. $.app.show("错误消息{0}".format(e.reason))
    26. }, [local_node.node_host, local_node.node_port, local_node.node_version])

     看看最终的效果

    20220814_213300

    https://live.csdn.net/v/231793

  • 相关阅读:
    学习ASP.NET Core Blazor编程系列三——实体
    基于神经网络的帧内预测和变换核选择
    Kotlin协程:挂起与恢复原理逆向刨析
    FactoryBean解读
    ✔ ★算法基础笔记(Acwing)(二)—— 数据结构(17道题)【java版本】
    linux控制组: cpuset解析
    深度学习的模型压缩与加速(万字长文带你入门)
    智慧能源太阳能光伏数据采集终端钡铼技术4G无线RTU
    Java部分面试题(宝典篇)
    多线程---锁策略与CAS
  • 原文地址:https://blog.csdn.net/inthirties/article/details/126334040