add terminal #17

This commit is contained in:
0xJacky 2022-07-06 19:46:27 +08:00
parent a571bccb84
commit 101c374b9c
16 changed files with 439 additions and 98 deletions

View file

@ -1,9 +1,9 @@
{
"name": "nginx-ui-frontend",
"version": "1.3.2",
"version": "1.4.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"serve": "vue-cli-service serve --port 8021",
"build": "vue-cli-service build --dest dist --modern",
"lint": "vue-cli-service lint"
},
@ -26,6 +26,7 @@
"core-js": "^3.9.0",
"less": "^3.11.1",
"less-loader": "^5.0.0",
"lodash": "^4.17.21",
"lowlight": "^1.20.0",
"moment": "^2.24.0",
"node-sass": "^6.0.1",
@ -46,7 +47,10 @@
"vue-template-compiler": "^2.6.11",
"vue2-ace-editor": "^0.0.15",
"vuex": "^3.6.2",
"vuex-persist": "^3.1.3"
"vuex-persist": "^3.1.3",
"xterm": "^4.19.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.5.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.15",

View file

@ -10,11 +10,11 @@ msgstr ""
"Generated-By: easygettext\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/router/index.js:99
#: src/router/index.js:107
msgid "404 Not Found"
msgstr ""
#: src/router/index.js:77
#: src/router/index.js:85
msgid "About"
msgstr ""
@ -119,7 +119,7 @@ msgstr ""
msgid "Destroy"
msgstr ""
#: src/router/index.js:125
#: src/router/index.js:133
msgid "Detected version update, this page will refresh."
msgstr ""
@ -244,7 +244,7 @@ msgstr ""
msgid "Index (index)"
msgstr ""
#: src/router/index.js:87 src/views/other/Install.vue:51
#: src/router/index.js:95 src/views/other/Install.vue:51
msgid "Install"
msgstr ""
@ -269,7 +269,7 @@ msgstr ""
msgid "Load Averages:"
msgstr ""
#: src/router/index.js:93 src/views/other/Login.vue:25
#: src/router/index.js:101 src/views/other/Login.vue:25
msgid "Login"
msgstr ""
@ -339,7 +339,7 @@ msgstr ""
msgid "No, I'm rethink"
msgstr ""
#: src/router/index.js:105
#: src/router/index.js:113
msgid "Not Found"
msgstr ""
@ -353,7 +353,7 @@ msgid ""
"you need to get the certificate."
msgstr ""
#: src/router/index.js:129
#: src/router/index.js:137
msgid "OK"
msgstr ""
@ -460,10 +460,14 @@ msgstr ""
msgid "Swap"
msgstr ""
#: src/router/index.js:124
#: src/router/index.js:132
msgid "System message"
msgstr ""
#: src/router/index.js:77
msgid "Terminal"
msgstr ""
#: src/views/domain/columns.js:50
msgid ""
"The certificate for the domain will be checked every hour, and will be "

View file

@ -12,11 +12,11 @@ msgstr ""
"Generated-By: easygettext\n"
"X-Generator: Poedit 3.0.1\n"
#: src/router/index.js:99
#: src/router/index.js:107
msgid "404 Not Found"
msgstr "404 未找到页面"
#: src/router/index.js:77
#: src/router/index.js:85
msgid "About"
msgstr "关于"
@ -121,7 +121,7 @@ msgstr "数据库 (可选,默认: database)"
msgid "Destroy"
msgstr "删除"
#: src/router/index.js:125
#: src/router/index.js:133
msgid "Detected version update, this page will refresh."
msgstr "检测到版本更新,页面将会刷新。"
@ -246,7 +246,7 @@ msgstr "HTTPS 监听端口"
msgid "Index (index)"
msgstr "网站首页 (index)"
#: src/router/index.js:87 src/views/other/Install.vue:51
#: src/router/index.js:95 src/views/other/Install.vue:51
msgid "Install"
msgstr "安装"
@ -271,7 +271,7 @@ msgstr "开源许可"
msgid "Load Averages:"
msgstr "系统负载:"
#: src/router/index.js:93 src/views/other/Login.vue:25
#: src/router/index.js:101 src/views/other/Login.vue:25
msgid "Login"
msgstr "登录"
@ -343,7 +343,7 @@ msgstr "下一步"
msgid "No, I'm rethink"
msgstr "再想想"
#: src/router/index.js:105
#: src/router/index.js:113
msgid "Not Found"
msgstr "找不到页面"
@ -357,7 +357,7 @@ msgid ""
"you need to get the certificate."
msgstr "注意:当前配置中的 server_name 必须为需要申请证书的域名。"
#: src/router/index.js:129
#: src/router/index.js:137
msgid "OK"
msgstr "确定"
@ -464,10 +464,14 @@ msgstr "主体名称: %{name}"
msgid "Swap"
msgstr ""
#: src/router/index.js:124
#: src/router/index.js:132
msgid "System message"
msgstr "系统消息"
#: src/router/index.js:77
msgid "Terminal"
msgstr "终端"
#: src/views/domain/columns.js:50
msgid ""
"The certificate for the domain will be checked every hour, and will be "
@ -488,8 +492,8 @@ msgid ""
"fields in your configuration file. The configuration filename cannot be "
"changed after it has been created."
msgstr ""
"只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不"
"修改。"
"只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不"
"修改。"
#: src/views/domain/DomainAdd.vue:15 src/views/domain/DomainAdd.vue:4
#: src/views/domain/DomainEdit.vue:24 src/views/domain/DomainEdit.vue:5

View file

@ -13,11 +13,11 @@ msgstr ""
"Generated-By: easygettext\n"
"X-Generator: Poedit 3.0.1\n"
#: src/router/index.js:99
#: src/router/index.js:107
msgid "404 Not Found"
msgstr "404 未找到頁面"
#: src/router/index.js:77
#: src/router/index.js:85
msgid "About"
msgstr "關於"
@ -122,7 +122,7 @@ msgstr "資料庫 (可選,預設: database)"
msgid "Destroy"
msgstr "删除"
#: src/router/index.js:125
#: src/router/index.js:133
msgid "Detected version update, this page will refresh."
msgstr "檢測到版本更新,頁面將會重新整理。"
@ -247,7 +247,7 @@ msgstr "HTTPS 監聽埠"
msgid "Index (index)"
msgstr "網站首頁 (index)"
#: src/router/index.js:87 src/views/other/Install.vue:51
#: src/router/index.js:95 src/views/other/Install.vue:51
msgid "Install"
msgstr "安裝"
@ -272,7 +272,7 @@ msgstr "開源許可"
msgid "Load Averages:"
msgstr "系統負載:"
#: src/router/index.js:93 src/views/other/Login.vue:25
#: src/router/index.js:101 src/views/other/Login.vue:25
msgid "Login"
msgstr "登入"
@ -344,7 +344,7 @@ msgstr "下一步"
msgid "No, I'm rethink"
msgstr "再想想"
#: src/router/index.js:105
#: src/router/index.js:113
msgid "Not Found"
msgstr "找不到頁面"
@ -358,7 +358,7 @@ msgid ""
"you need to get the certificate."
msgstr "注意:當前配置中的 server_name 必須為需要申請證書的域名。"
#: src/router/index.js:129
#: src/router/index.js:137
msgid "OK"
msgstr "確定"
@ -465,10 +465,14 @@ msgstr "主體名稱: %{name}"
msgid "Swap"
msgstr "交換空間"
#: src/router/index.js:124
#: src/router/index.js:132
msgid "System message"
msgstr "系統訊息"
#: src/router/index.js:77
msgid "Terminal"
msgstr "终端"
#: src/views/domain/columns.js:50
msgid ""
"The certificate for the domain will be checked every hour, and will be "
@ -489,8 +493,8 @@ msgid ""
"fields in your configuration file. The configuration filename cannot be "
"changed after it has been created."
msgstr ""
"只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可"
"改。"
"只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可"
"改。"
#: src/views/domain/DomainAdd.vue:15 src/views/domain/DomainAdd.vue:4
#: src/views/domain/DomainEdit.vue:24 src/views/domain/DomainEdit.vue:5

View file

@ -72,6 +72,14 @@ export const routes = [
hiddenInSidebar: true
},
},
{
path: 'terminal',
name: $gettext('Terminal'),
component: () => import('@/views/pty/Terminal'),
meta: {
icon: 'code'
}
},
{
path: 'about',
name: $gettext('About'),

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,98 @@
<template>
<a-card :title="$gettext('Terminal')">
<div class="console" id="terminal"></div>
</a-card>
</template>
<script>
import ReconnectingWebSocket from 'reconnecting-websocket'
import 'xterm/css/xterm.css'
import {Terminal} from 'xterm'
import {FitAddon} from 'xterm-addon-fit'
const _ = require('lodash')
export default {
name: 'Terminal',
data() {
return {
term: null,
ping: null
}
},
created() {
this.websocket = new ReconnectingWebSocket(this.getWebSocketRoot() + '/pty?token='
+ btoa(this.$store.state.user.token))
this.websocket.onmessage = this.wsOnMessage
this.websocket.onopen = this.wsOnOpen
},
mounted() {
this.initTerm()
},
destroyed() {
window.removeEventListener('resize', this.fit)
clearInterval(this.ping)
this.ping = null
this.websocket.close()
},
methods: {
fit: _.throttle(function () {
this.fitAddon.fit()
}, 50),
initTerm() {
const term = new Terminal({
rendererType: 'canvas',
convertEol: true,
fontSize: 14,
cursorStyle: 'block',
scrollback: 30,
})
const fitAddon = new FitAddon()
term.loadAddon(fitAddon)
this.fitAddon = fitAddon
term.open(document.getElementById('terminal'))
setTimeout(()=>{
fitAddon.fit()
}, 60)
window.addEventListener('resize', this.fit)
term.focus()
let that = this
term.onData(function (key) {
let order = {
Data: key,
Type: 1
}
that.sendMessage(order)
})
term.onBinary(data => {
that.sendMessage({Type: 1, Data: data})
})
term.onResize(data => {
that.sendMessage({Type:2, Data:{Cols:data.cols, Rows: data.rows}})
})
this.term = term
},
wsOnMessage(msg) {
this.term.write(msg.data)
},
wsOnOpen() {
const that = this
this.ping = setInterval(function () {
that.sendMessage({Type: 3})
}, 10000)
},
sendMessage(data) {
this.websocket.send(JSON.stringify(data))
}
}
}
</script>
<style lang="less" scoped>
.console {
min-height: 800px;
}
</style>

View file

@ -6963,7 +6963,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npm.taobao.org/lodash.uniq/download/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@4.17.21, lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.5, lodash@~4.17.10:
lodash@4.17.21, lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.5, lodash@~4.17.10:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -11196,6 +11196,21 @@ xtend@^4.0.0, xtend@~4.0.1:
resolved "https://registry.npm.taobao.org/xtend/download/xtend-4.0.2.tgz?cache=0&sync_timestamp=1589682817913&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fxtend%2Fdownload%2Fxtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=
xterm-addon-attach@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz#220c23addd62ab88c9914e2d4c06f7407e44680e"
integrity sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==
xterm-addon-fit@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
xterm@^4.19.0:
version "4.19.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d"
integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==
y18n@^4.0.0:
version "4.0.1"
resolved "https://registry.npm.taobao.org/y18n/download/y18n-4.0.1.tgz?cache=0&sync_timestamp=1609798661541&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fy18n%2Fdownload%2Fy18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"

1
go.mod
View file

@ -27,6 +27,7 @@ require (
require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/golang/protobuf v1.3.4 // indirect

2
go.sum
View file

@ -80,6 +80,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

46
server/api/pty.go Normal file
View file

@ -0,0 +1,46 @@
package api
import (
"github.com/0xJacky/Nginx-UI/server/tool/pty"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"log"
"net/http"
)
func Pty(c *gin.Context) {
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// upgrade http to websocket
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println("pty ws upgrade error", err)
return
}
defer ws.Close()
p, err := pty.NewPipeLine(ws)
if err != nil {
log.Println("pty.NewPipLine error", err)
return
}
defer p.Pty.Close()
errorChan := make(chan error, 1)
go p.ReadPtyAndWriteWs(errorChan)
go p.ReadWsAndWritePty(errorChan)
err = <-errorChan
if err != nil {
log.Println(err)
}
return
}

View file

@ -1,89 +1,92 @@
package router
import (
"bufio"
"github.com/0xJacky/Nginx-UI/server/api"
"github.com/0xJacky/Nginx-UI/server/settings"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"bufio"
"github.com/0xJacky/Nginx-UI/server/api"
"github.com/0xJacky/Nginx-UI/server/settings"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r := gin.New()
r.Use(gin.Logger())
r.Use(recovery())
r.Use(recovery())
r.Use(cacheJs())
r.Use(cacheJs())
r.Use(static.Serve("/", mustFS("")))
r.Use(static.Serve("/", mustFS("")))
r.NoRoute(func(c *gin.Context) {
accept := c.Request.Header.Get("Accept")
if strings.Contains(accept, "text/html") {
file, _ := mustFS("").Open("index.html")
defer file.Close()
stat, _ := file.Stat()
c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
bufio.NewReader(file), nil)
return
}
})
r.NoRoute(func(c *gin.Context) {
accept := c.Request.Header.Get("Accept")
if strings.Contains(accept, "text/html") {
file, _ := mustFS("").Open("index.html")
defer file.Close()
stat, _ := file.Stat()
c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
bufio.NewReader(file), nil)
return
}
})
g := r.Group("/api")
{
g := r.Group("/api")
{
g.GET("settings", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"demo": settings.ServerSettings.Demo,
})
})
g.GET("settings", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"demo": settings.ServerSettings.Demo,
})
})
g.GET("install", api.InstallLockCheck)
g.POST("install", api.InstallNginxUI)
g.GET("install", api.InstallLockCheck)
g.POST("install", api.InstallNginxUI)
g.POST("/login", api.Login)
g.DELETE("/logout", api.Logout)
g.POST("/login", api.Login)
g.DELETE("/logout", api.Logout)
g := g.Group("/", authRequired())
{
g.GET("/analytic", api.Analytic)
g.GET("/analytic/init", api.GetAnalyticInit)
g := g.Group("/", authRequired())
{
g.GET("/analytic", api.Analytic)
g.GET("/analytic/init", api.GetAnalyticInit)
g.GET("/users", api.GetUsers)
g.GET("/user/:id", api.GetUser)
g.POST("/user", api.AddUser)
g.POST("/user/:id", api.EditUser)
g.DELETE("/user/:id", api.DeleteUser)
g.GET("/users", api.GetUsers)
g.GET("/user/:id", api.GetUser)
g.POST("/user", api.AddUser)
g.POST("/user/:id", api.EditUser)
g.DELETE("/user/:id", api.DeleteUser)
g.GET("domains", api.GetDomains)
g.GET("domain/:name", api.GetDomain)
g.POST("domain/:name", api.EditDomain)
g.POST("domain/:name/enable", api.EnableDomain)
g.POST("domain/:name/disable", api.DisableDomain)
g.DELETE("domain/:name", api.DeleteDomain)
g.GET("domains", api.GetDomains)
g.GET("domain/:name", api.GetDomain)
g.POST("domain/:name", api.EditDomain)
g.POST("domain/:name/enable", api.EnableDomain)
g.POST("domain/:name/disable", api.DisableDomain)
g.DELETE("domain/:name", api.DeleteDomain)
g.GET("configs", api.GetConfigs)
g.GET("config/:name", api.GetConfig)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
g.GET("configs", api.GetConfigs)
g.GET("config/:name", api.GetConfig)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
g.GET("backups", api.GetFileBackupList)
g.GET("backup/:id", api.GetFileBackup)
g.GET("backups", api.GetFileBackupList)
g.GET("backup/:id", api.GetFileBackup)
g.GET("template/:name", api.GetTemplate)
g.GET("template/:name", api.GetTemplate)
g.GET("cert/issue/:domain", api.IssueCert)
g.GET("cert/:domain/info", api.CertInfo)
g.GET("cert/issue/:domain", api.IssueCert)
g.GET("cert/:domain/info", api.CertInfo)
// 添加域名到自动续期列表
g.POST("cert/:domain", api.AddDomainToAutoCert)
// 从自动续期列表中删除域名
g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
}
}
// 添加域名到自动续期列表
g.POST("cert/:domain", api.AddDomainToAutoCert)
// 从自动续期列表中删除域名
g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
return r
// pty
g.GET("pty", api.Pty)
}
}
return r
}

142
server/tool/pty/pipeline.go Normal file
View file

@ -0,0 +1,142 @@
package pty
import (
"encoding/json"
"github.com/creack/pty"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"os"
"os/exec"
"time"
"unicode/utf8"
)
type Pipeline struct {
Pty *os.File
ws *websocket.Conn
}
type Message struct {
Type MsgType
Data json.RawMessage
}
const bufferSize = 2048
func NewPipeLine(conn *websocket.Conn) (p *Pipeline, err error) {
c := exec.Command("login")
ptmx, err := pty.StartWithSize(c, &pty.Winsize{Cols: 90, Rows: 60})
if err != nil {
return nil, errors.Wrap(err, "start pty error")
}
p = &Pipeline{
Pty: ptmx,
ws: conn,
}
return
}
func (p *Pipeline) ReadWsAndWritePty(errorChan chan error) {
for {
msgType, payload, err := p.ws.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived,
websocket.CloseNormalClosure) {
errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty unexpected close")
}
return
}
if msgType != websocket.TextMessage {
errorChan <- errors.Errorf("Error ReadWsAndWritePty Invalid msgType: %v", msgType)
return
}
var msg Message
err = json.Unmarshal(payload, &msg)
if err != nil {
errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty json.Unmarshal")
return
}
switch msg.Type {
case TypeData:
var data string
err = json.Unmarshal(msg.Data, &data)
if err != nil {
errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty json.Unmarshal msg.Data")
return
}
_, err = p.Pty.Write([]byte(data))
if err != nil {
errorChan <- errors.Wrap(err, "Error ReadWsAndWritePty write pty")
return
}
case TypeResize:
var win struct {
Cols uint16
Rows uint16
}
err = json.Unmarshal(msg.Data, &win)
if err != nil {
errorChan <- errors.Wrap(err, "Error ReadSktAndWritePty Invalid resize message")
return
}
err = pty.Setsize(p.Pty, &pty.Winsize{Rows: win.Rows, Cols: win.Cols})
if err != nil {
errorChan <- errors.Wrap(err, "Error ReadSktAndWritePty set pty size")
return
}
case TypePing:
err = p.ws.WriteControl(websocket.PongMessage, []byte{}, time.Now().Add(time.Second))
if err != nil {
errorChan <- errors.Wrap(err, "Error ReadSktAndWritePty write pong")
return
}
default:
errorChan <- errors.Errorf("Error ReadWsAndWritePty unknown msg.Type %v", msg.Type)
return
}
}
}
func (p *Pipeline) ReadPtyAndWriteWs(errorChan chan error) {
buf := make([]byte, bufferSize)
for {
n, err := p.Pty.Read(buf)
if err != nil {
errorChan <- errors.Wrap(err, "Error ReadPtyAndWriteWs read pty")
return
}
processedOutput := validString(string(buf[:n]))
err = p.ws.WriteMessage(websocket.TextMessage, []byte(processedOutput))
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
errorChan <- errors.Wrap(err, "Error ReadPtyAndWriteWs websocket write")
}
return
}
}
}
func validString(s string) string {
if !utf8.ValidString(s) {
v := make([]rune, 0, len(s))
for i, r := range s {
if r == utf8.RuneError {
_, size := utf8.DecodeRuneInString(s[i:])
if size == 1 {
continue
}
}
v = append(v, r)
}
s = string(v)
}
return s
}

10
server/tool/pty/type.go Normal file
View file

@ -0,0 +1,10 @@
package pty
type MsgType int
const (
MsgTypeInit MsgType = iota
TypeData
TypeResize
TypePing
)