基于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>
截图