基于windows,golang和vue通过xterm实现伪终端

基于windows,golang和vue通过xterm实现终端

main.go

/*
 * @Description:
 * @Version: 1.0
 * @Autor: solid
 * @Date: 2022-08-24 18:37:22
 * @LastEditors: solid
 * @LastEditTime: 2022-09-16 14:56:15
 */
package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"unicode/utf8"

	"gitee.com/solidone/sutils/swebsocket"
	"github.com/gin-gonic/gin"
	"github.com/iamacarpet/go-winpty"

	"github.com/gorilla/websocket"
)

var Upgrader *websocket.Upgrader = &websocket.Upgrader{
	ReadBufferSize:  2 * 1024,
	WriteBufferSize: 2 * 1024,
	// Allow connections from any Origin
	CheckOrigin: func(r *http.Request) bool { return true },
}

type Response struct {
	Code int         `json:"code"`
	Msg  string      `json:"message"`
	Data interface{} `json:"data"`
}
type Message struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"`
}

func Connect(ctx *gin.Context) {
	wsConn, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	ClientConn, _ := swebsocket.CreateConn(wsConn, 1)
	Pty, err := winpty.OpenDefault("", "cmd")
	if err != nil {
		log.Fatalf("Failed to start command: %s\n", err)
	}
	//Set the size of the pty
	Pty.SetSize(200, 60)
	go func() {
		buf := make([]byte, 8192)
		reader := bufio.NewReader(Pty.StdOut)
		var buffer bytes.Buffer
		for {
			n, err := reader.Read(buf)
			if err != nil {
				log.Printf("Failed to read from pty master: %s", err)
				return
			}
			//read byte array as Unicode code points (rune in go)
			bufferBytes := buffer.Bytes()
			runeReader := bufio.NewReader(bytes.NewReader(append(bufferBytes[:], buf[:n]...)))
			buffer.Reset()
			i := 0
			for i < n {
				char, charLen, e := runeReader.ReadRune()
				if e != nil {
					log.Printf("Failed to read from pty master: %s", err)
					return
				}
				if char == utf8.RuneError {
					runeReader.UnreadRune()
					break
				}
				i += charLen
				buffer.WriteRune(char)
			}
			ClientConn.Send <- buffer.Bytes()
			buffer.Reset()
			if i < n {
				buffer.Write(buf[i:n])
			}
		}
	}()
	ClientConn.Handle(func(msg []byte, conn *swebsocket.ServerConn) {
		var res Message
		err := json.Unmarshal(msg, &res)
		if err != nil {
			return
		}
		switch res.Type {
		case "resize":
			var size []float64
			err := json.Unmarshal(res.Data, &size)
			if err != nil {
				log.Printf("Invalid resize message: %s\n", err)
			} else {
				Pty.SetSize(uint32(size[0]), uint32(size[1]))
			}
		case "data":
			var dat string
			err := json.Unmarshal(res.Data, &dat)
			if err != nil {
				log.Printf("Invalid data message %s\n", err)
			} else {
				Pty.StdIn.Write([]byte(dat))
			}
		}
	})
	ClientConn.WriteReadLoop()
	Pty.Close()
}

func main() {
	router := gin.Default()
	//跨域
	router.Use(Cors())

	router.GET("/ws", Connect)

	err := router.Run(":8888")
	if err != nil {
		fmt.Println("Init http server. Error :", err)
	}

}

// 跨域
func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method               // 请求方法
		origin := c.Request.Header.Get("Origin") // 请求头部

		var headerKeys []string // 声明请求头keys
		for k := range c.Request.Header {
			headerKeys = append(headerKeys, k)
		}
		headerStr := strings.Join(headerKeys, ", ")
		if headerStr == "" {
			headerStr = "access-control-allow-origin, access-control-allow-headers"
		}
		if origin != "" {
			c.Header("Access-Control-Allow-Origin", origin)                                    // 这是允许访问所有域
			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") // 服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
			//  header的类型
			c.Header("Access-Control-Allow-Headers", "*")
			//              允许跨域设置                                                                                                      可以返回其他子段
			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
			c.Header("Access-Control-Max-Age", "172800")                                                                                                                                                           // 缓存请求信息 单位为秒
			c.Header("Access-Control-Allow-Credentials", "true")                                                                                                                                                   //  跨域请求是否需要带cookie信息 默认设置为true
			c.Set("content-type", "application/json")                                                                                                                                                              // 设置返回格式是json
		}

		// 放行所有OPTIONS方法
		if method == "OPTIONS" {
			c.JSON(http.StatusOK, "Options Request!")
		}
		// 处理请求
		c.Next() //  处理请求
	}
}


前端

安装相关依赖

 "element-plus": "^2.7.0",
    "lodash": "^4.17.21",
    "vue": "^3.3.11",
    "xterm": "^5.3.0",
    "xterm-addon-fit": "^0.8.0"

核心代码

<template>
    <div id="terminal" v-loading="loading" class="terminal" element-loading-text="拼命连接中"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { debounce } from 'lodash'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'

const terminal = ref(null)
const fitAddon = new FitAddon()

let first = ref(true)
let loading = ref(true)
let terminalSocket = ref(null)
let term = null
var timer;
var setSize = function () {
  let rect = document.getElementById("terminal").getBoundingClientRect()
  console.log(document.documentElement.clientHeight, document.documentElement.clientWidth);
  let cols = Math.floor(rect.width / 13);
  let rows = Math.floor(rect.width.height / 23);
  term.resize(cols, rows);
  terminalSocket.value.send(JSON.stringify({ type: "resize", "data": [cols, rows] }));
};
const fitTerm=()=>{
  fitAddon.fit();
}
const onResize = debounce(() => fitTerm(), 500)
const onTerminalResize = () => {
    window.addEventListener('resize', onResize)
  }
  const removeResizeListener = () => {
    window.removeEventListener('resize', onResize)
  }
const initSocket = () => {
  terminalSocket.value = new WebSocket("ws://127.0.0.1:8888/ws")
  terminalSocket.value.onopen = () => {
    onTerminalResize()
    term = new Terminal({
      fontSize: 16,
      cursor_style: "block",
      cursorBlink: true,
      theme: {
        foreground: "#ECECEC", //字体
        background: "#000000", //背景色
        cursor: "help", //设置光标
        lineHeight: 20,
      },
        // 光标闪烁
        cursorBlink: true,
      cursorStyle: 'underline',
    });
    term.loadAddon(fitAddon);
    term.open(document.getElementById("terminal"));
    fitAddon.fit();
   
    term.focus();
    const { cols, rows } = term
    console.log(cols, rows);
    term.onData((data) => {
      terminalSocket.value.send(JSON.stringify({ type: "data", "data": data }));
    })
    term.onResize((data) => {
      const { cols, rows } = term
      console.log(data.cols, data.rows);
      terminalSocket.value.send(JSON.stringify({ type: "resize", "data": [data.cols, data.rows] }));
    })
  }
  terminalSocket.value.onmessage = function (msg) {
    term.write(msg.data);
  };
}
onMounted(() => {
  initSocket()

})

onBeforeUnmount(() => {
  removeResizeListener()
  terminalSocket.value && terminalSocket.value.close()
})
</script>
<style>
#terminal {
  width: 100%;
  height: calc(100% - 62px);
}
</style>

截图

在这里插入图片描述

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值