mirror of
https://gitee.com/fit2cloud-feizhiyun/1Panel.git
synced 2024-11-29 18:37:41 +08:00
feat: 本地连接重新实现,移除 gotty
This commit is contained in:
parent
0f1ff3300e
commit
7089775109
@ -79,6 +79,48 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) LocalWsSsh(c *gin.Context) {
|
||||||
|
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("gin context http handler failed, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer wsConn.Close()
|
||||||
|
|
||||||
|
slave, err := terminal.NewCommand()
|
||||||
|
if wshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer slave.Close()
|
||||||
|
|
||||||
|
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave)
|
||||||
|
if wshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
quitChan := make(chan bool, 3)
|
||||||
|
tty.Start(quitChan)
|
||||||
|
go slave.Wait(quitChan)
|
||||||
|
|
||||||
|
<-quitChan
|
||||||
|
|
||||||
|
global.LOG.Info("websocket finished")
|
||||||
|
if wshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func wshandleError(ws *websocket.Conn, err error) bool {
|
func wshandleError(ws *websocket.Conn, err error) bool {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("handler ws faled:, err: %v", err)
|
global.LOG.Errorf("handler ws faled:, err: %v", err)
|
||||||
|
@ -3,7 +3,7 @@ package dto
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type HostCreate struct {
|
type HostCreate struct {
|
||||||
Name string `json:"name" validate:"required,name"`
|
Name string `json:"name" validate:"required"`
|
||||||
Addr string `json:"addr" validate:"required,ip"`
|
Addr string `json:"addr" validate:"required,ip"`
|
||||||
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
||||||
User string `json:"user" validate:"required"`
|
User string `json:"user" validate:"required"`
|
||||||
@ -27,7 +27,7 @@ type HostInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HostUpdate struct {
|
type HostUpdate struct {
|
||||||
Name string `json:"name" validate:"required,name"`
|
Name string `json:"name" validate:"required"`
|
||||||
Addr string `json:"addr" validate:"required,ip"`
|
Addr string `json:"addr" validate:"required,ip"`
|
||||||
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
||||||
User string `json:"user" validate:"required"`
|
User string `json:"user" validate:"required"`
|
||||||
|
@ -15,6 +15,7 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/gwatts/gin-adapter v1.0.0
|
github.com/gwatts/gin-adapter v1.0.0
|
||||||
github.com/jinzhu/copier v0.3.5
|
github.com/jinzhu/copier v0.3.5
|
||||||
|
github.com/kr/pty v1.1.1
|
||||||
github.com/mojocn/base64Captcha v1.3.5
|
github.com/mojocn/base64Captcha v1.3.5
|
||||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
||||||
|
@ -257,6 +257,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
package binary
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/global"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StartTTY() {
|
|
||||||
cmd := "gotty"
|
|
||||||
params := []string{"--permit-write", "bash"}
|
|
||||||
go func() {
|
|
||||||
c := exec.Command(cmd, params...)
|
|
||||||
c.Env = append(c.Env, os.Environ()...)
|
|
||||||
c.Stdout = io.Discard
|
|
||||||
c.Stderr = io.Discard
|
|
||||||
if err := c.Run(); err != nil {
|
|
||||||
global.LOG.Error(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
@ -2,8 +2,10 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -16,11 +18,24 @@ import (
|
|||||||
func OperationRecord() gin.HandlerFunc {
|
func OperationRecord() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var body []byte
|
var body []byte
|
||||||
if c.Request.Method == http.MethodGet || strings.Contains(c.Request.URL.Path, "search") {
|
if strings.Contains(c.Request.URL.Path, "search") {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Request.Method == http.MethodGet {
|
||||||
|
query := c.Request.URL.RawQuery
|
||||||
|
query, _ = url.QueryUnescape(query)
|
||||||
|
split := strings.Split(query, "&")
|
||||||
|
m := make(map[string]string)
|
||||||
|
for _, v := range split {
|
||||||
|
kv := strings.Split(v, "=")
|
||||||
|
if len(kv) == 2 {
|
||||||
|
m[kv[0]] = kv[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body, _ = json.Marshal(&m)
|
||||||
|
} else {
|
||||||
var err error
|
var err error
|
||||||
body, err = ioutil.ReadAll(c.Request.Body)
|
body, err = ioutil.ReadAll(c.Request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -28,6 +43,7 @@ func OperationRecord() gin.HandlerFunc {
|
|||||||
} else {
|
} else {
|
||||||
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pathInfo := loadLogInfo(c.Request.URL.Path)
|
pathInfo := loadLogInfo(c.Request.URL.Path)
|
||||||
|
|
||||||
record := model.OperationLog{
|
record := model.OperationLog{
|
||||||
|
@ -2,7 +2,6 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
||||||
"github.com/1Panel-dev/1Panel/middleware"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -11,9 +10,9 @@ type TerminalRouter struct{}
|
|||||||
|
|
||||||
func (s *UserRouter) InitTerminalRouter(Router *gin.RouterGroup) {
|
func (s *UserRouter) InitTerminalRouter(Router *gin.RouterGroup) {
|
||||||
terminalRouter := Router.Group("terminals")
|
terminalRouter := Router.Group("terminals")
|
||||||
withRecordRouter := terminalRouter.Use(middleware.OperationRecord())
|
|
||||||
baseApi := v1.ApiGroupApp.BaseApi
|
baseApi := v1.ApiGroupApp.BaseApi
|
||||||
{
|
{
|
||||||
withRecordRouter.GET("", baseApi.WsSsh)
|
terminalRouter.GET("", baseApi.WsSsh)
|
||||||
|
terminalRouter.GET("/local", baseApi.LocalWsSsh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,13 @@ package server
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/init/cache"
|
"github.com/1Panel-dev/1Panel/init/cache"
|
||||||
"github.com/1Panel-dev/1Panel/init/session"
|
"github.com/1Panel-dev/1Panel/init/session"
|
||||||
"github.com/1Panel-dev/1Panel/init/session/psession"
|
"github.com/1Panel-dev/1Panel/init/session/psession"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/global"
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
"github.com/1Panel-dev/1Panel/init/binary"
|
|
||||||
"github.com/1Panel-dev/1Panel/init/db"
|
"github.com/1Panel-dev/1Panel/init/db"
|
||||||
"github.com/1Panel-dev/1Panel/init/log"
|
"github.com/1Panel-dev/1Panel/init/log"
|
||||||
"github.com/1Panel-dev/1Panel/init/migration"
|
"github.com/1Panel-dev/1Panel/init/migration"
|
||||||
@ -30,7 +30,6 @@ func Start() {
|
|||||||
gob.Register(psession.SessionUser{})
|
gob.Register(psession.SessionUser{})
|
||||||
cache.Init()
|
cache.Init()
|
||||||
session.Init()
|
session.Init()
|
||||||
binary.StartTTY()
|
|
||||||
gin.SetMode(global.CONF.System.Level)
|
gin.SetMode(global.CONF.System.Level)
|
||||||
|
|
||||||
routers := router.Routers()
|
routers := router.Routers()
|
||||||
|
115
backend/utils/terminal/local_cmd.go
Normal file
115
backend/utils/terminal/local_cmd.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"github.com/kr/pty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultCloseSignal = syscall.SIGINT
|
||||||
|
DefaultCloseTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalCommand struct {
|
||||||
|
command string
|
||||||
|
|
||||||
|
closeSignal syscall.Signal
|
||||||
|
closeTimeout time.Duration
|
||||||
|
|
||||||
|
cmd *exec.Cmd
|
||||||
|
pty *os.File
|
||||||
|
ptyClosed chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommand() (*LocalCommand, error) {
|
||||||
|
command := "sh"
|
||||||
|
cmd := exec.Command(command)
|
||||||
|
cmd.Dir = "/"
|
||||||
|
|
||||||
|
pty, err := pty.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to start command `%s`", command)
|
||||||
|
}
|
||||||
|
ptyClosed := make(chan struct{})
|
||||||
|
|
||||||
|
lcmd := &LocalCommand{
|
||||||
|
command: command,
|
||||||
|
closeSignal: DefaultCloseSignal,
|
||||||
|
closeTimeout: DefaultCloseTimeout,
|
||||||
|
|
||||||
|
cmd: cmd,
|
||||||
|
pty: pty,
|
||||||
|
ptyClosed: ptyClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
return lcmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcmd *LocalCommand) Read(p []byte) (n int, err error) {
|
||||||
|
return lcmd.pty.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcmd *LocalCommand) Write(p []byte) (n int, err error) {
|
||||||
|
return lcmd.pty.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcmd *LocalCommand) Close() error {
|
||||||
|
if lcmd.cmd != nil && lcmd.cmd.Process != nil {
|
||||||
|
_ = lcmd.cmd.Process.Signal(lcmd.closeSignal)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-lcmd.ptyClosed:
|
||||||
|
return nil
|
||||||
|
case <-lcmd.closeTimeoutC():
|
||||||
|
_ = lcmd.cmd.Process.Signal(syscall.SIGKILL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error {
|
||||||
|
window := struct {
|
||||||
|
row uint16
|
||||||
|
col uint16
|
||||||
|
x uint16
|
||||||
|
y uint16
|
||||||
|
}{
|
||||||
|
uint16(height),
|
||||||
|
uint16(width),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
_, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
lcmd.pty.Fd(),
|
||||||
|
syscall.TIOCSWINSZ,
|
||||||
|
uintptr(unsafe.Pointer(&window)),
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return errno
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcmd *LocalCommand) Wait(quitChan chan bool) {
|
||||||
|
if err := lcmd.cmd.Wait(); err != nil {
|
||||||
|
global.LOG.Errorf("ssh session wait failed, err: %v", err)
|
||||||
|
setQuit(quitChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lcmd *LocalCommand) closeTimeoutC() <-chan time.Time {
|
||||||
|
if lcmd.closeTimeout >= 0 {
|
||||||
|
return time.After(lcmd.closeTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
return make(chan time.Time)
|
||||||
|
}
|
108
backend/utils/terminal/ws_local_session.go
Normal file
108
backend/utils/terminal/ws_local_session.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalWsSession struct {
|
||||||
|
slave *LocalCommand
|
||||||
|
wsConn *websocket.Conn
|
||||||
|
|
||||||
|
writeMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand) (*LocalWsSession, error) {
|
||||||
|
if err := slave.ResizeTerminal(cols, rows); err != nil {
|
||||||
|
global.LOG.Errorf("ssh pty change windows size failed, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LocalWsSession{
|
||||||
|
slave: slave,
|
||||||
|
wsConn: wsConn,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sws *LocalWsSession) Start(quitChan chan bool) {
|
||||||
|
go sws.handleSlaveEvent(quitChan)
|
||||||
|
go sws.receiveWsMsg(quitChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) {
|
||||||
|
defer setQuit(exitCh)
|
||||||
|
|
||||||
|
buffer := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-exitCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
n, err := sws.slave.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("read buffer from slave failed, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sws.masterWrite(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("handle master read event failed, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sws *LocalWsSession) masterWrite(data []byte) error {
|
||||||
|
sws.writeMutex.Lock()
|
||||||
|
defer sws.writeMutex.Unlock()
|
||||||
|
err := sws.wsConn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to write to master")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
|
||||||
|
wsConn := sws.wsConn
|
||||||
|
defer setQuit(exitCh)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-exitCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, wsData, err := wsConn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("reading webSocket message failed, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgObj := wsMsg{}
|
||||||
|
if err := json.Unmarshal(wsData, &msgObj); err != nil {
|
||||||
|
global.LOG.Errorf("unmarshal websocket message %s failed, err: %v", wsData, err)
|
||||||
|
}
|
||||||
|
switch msgObj.Type {
|
||||||
|
case wsMsgResize:
|
||||||
|
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||||
|
if err := sws.slave.ResizeTerminal(msgObj.Cols, msgObj.Rows); err != nil {
|
||||||
|
global.LOG.Errorf("ssh pty change windows size failed, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case wsMsgCmd:
|
||||||
|
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
|
||||||
|
}
|
||||||
|
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
|
||||||
|
if _, err := sws.slave.Write(cmdBytes); err != nil {
|
||||||
|
global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
@ -192,10 +192,6 @@ func (sws *LogicSshWsSession) Wait(quitChan chan bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sws *LogicSshWsSession) LogString() string {
|
|
||||||
return sws.logBuff.buffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setQuit(ch chan bool) {
|
func setQuit(ch chan bool) {
|
||||||
ch <- true
|
ch <- true
|
||||||
}
|
}
|
||||||
|
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
@ -7244,6 +7244,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz",
|
||||||
"integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg=="
|
"integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg=="
|
||||||
},
|
},
|
||||||
|
"xterm-addon-fit": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ=="
|
||||||
|
},
|
||||||
"y18n": {
|
"y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
@ -40,7 +40,8 @@
|
|||||||
"vue-router": "^4.0.12",
|
"vue-router": "^4.0.12",
|
||||||
"vue3-seamless-scroll": "^1.2.0",
|
"vue3-seamless-scroll": "^1.2.0",
|
||||||
"xterm": "^4.19.0",
|
"xterm": "^4.19.0",
|
||||||
"xterm-addon-attach": "^0.6.0"
|
"xterm-addon-attach": "^0.6.0",
|
||||||
|
"xterm-addon-fit": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.0.1",
|
"@commitlint/cli": "^17.0.1",
|
||||||
|
@ -92,6 +92,7 @@ export default {
|
|||||||
connHistory: 'historys',
|
connHistory: 'historys',
|
||||||
hostHistory: 'History record',
|
hostHistory: 'History record',
|
||||||
addHost: 'Add Host',
|
addHost: 'Add Host',
|
||||||
|
localhost: 'Localhost',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
@ -100,6 +101,7 @@ export default {
|
|||||||
keyMode: 'PrivateKey',
|
keyMode: 'PrivateKey',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
key: 'Private Key',
|
key: 'Private Key',
|
||||||
|
emptyTerminal: 'No terminal is currently connected',
|
||||||
},
|
},
|
||||||
operations: {
|
operations: {
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -93,6 +93,7 @@ export default {
|
|||||||
connHistory: '历史连接',
|
connHistory: '历史连接',
|
||||||
hostHistory: '历史主机信息',
|
hostHistory: '历史主机信息',
|
||||||
addHost: '添加主机',
|
addHost: '添加主机',
|
||||||
|
localhost: '本地服务器',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
port: '端口',
|
port: '端口',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
@ -101,6 +102,7 @@ export default {
|
|||||||
keyMode: '密钥输入',
|
keyMode: '密钥输入',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
key: '密钥',
|
key: '密钥',
|
||||||
|
emptyTerminal: '暂无终端连接',
|
||||||
},
|
},
|
||||||
operations: {
|
operations: {
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
v-model="terminalValue"
|
v-model="terminalValue"
|
||||||
@edit="handleTabsEdit"
|
@edit="handleTabsEdit"
|
||||||
>
|
>
|
||||||
<el-tab-pane :key="item.name" v-for="item in terminalTabs" :label="item.title" :name="item.name">
|
<el-tab-pane :key="item.key" v-for="item in terminalTabs" :label="item.title" :name="item.key">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="custom-tabs-label">
|
<span class="custom-tabs-label">
|
||||||
<el-icon color="#67C23A" v-if="item.status === 'online'"><circleCheck /></el-icon>
|
<el-icon color="#67C23A" v-if="item.status === 'online'"><circleCheck /></el-icon>
|
||||||
@ -20,24 +20,40 @@
|
|||||||
<span> {{ item.title }} </span>
|
<span> {{ item.title }} </span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<iframe
|
<Terminal
|
||||||
v-if="item.type === 'local'"
|
style="height: calc(100vh - 265px); background-color: #000"
|
||||||
id="iframeTerminal"
|
:ref="'Ref' + item.key"
|
||||||
name="iframeTerminal"
|
:wsID="item.wsID"
|
||||||
width="100%"
|
:terminalID="item.key"
|
||||||
frameborder="0"
|
></Terminal>
|
||||||
:src="item.src"
|
|
||||||
/>
|
|
||||||
<Terminal v-else :ref="'Ref' + item.name" :id="item.wsID"></Terminal>
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
<div v-if="terminalTabs.length === 0">
|
||||||
|
<el-empty
|
||||||
|
style="background-color: #000; height: calc(100vh - 265px)"
|
||||||
|
:description="$t('terminal.emptyTerminal')"
|
||||||
|
></el-empty>
|
||||||
|
</div>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-drawer :size="320" v-model="hostDrawer" :title="$t('terminal.hostHistory')" direction="rtl">
|
<el-drawer :size="320" v-model="hostDrawer" :title="$t('terminal.hostHistory')" direction="rtl">
|
||||||
<el-button @click="onAddHost">{{ $t('terminal.addHost') }}</el-button>
|
<el-button @click="onAddHost">{{ $t('terminal.addHost') }}</el-button>
|
||||||
<div v-infinite-scroll="nextPage" style="overflow: auto">
|
<div v-infinite-scroll="nextPage" style="overflow: auto">
|
||||||
|
<el-card
|
||||||
|
@click="onConnLocal()"
|
||||||
|
style="margin-top: 5px; cursor: pointer"
|
||||||
|
:title="$t('terminal.localhost')"
|
||||||
|
shadow="hover"
|
||||||
|
>
|
||||||
|
<div :inline="true">
|
||||||
|
<div>
|
||||||
|
<span>{{ $t('terminal.localhost') }}</span>
|
||||||
|
</div>
|
||||||
|
<span style="font-size: 14px; line-height: 25px"> [ 127.0.0.1 ]</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
<div v-for="(item, index) in data" :key="item.id" @mouseover="hover = index" @mouseleave="hover = null">
|
<div v-for="(item, index) in data" :key="item.id" @mouseover="hover = index" @mouseleave="hover = null">
|
||||||
<el-card @click="onConn(item)" style="margin-top: 5px" :title="item.name" shadow="hover">
|
<el-card @click="onConn(item)" style="margin-top: 5px; cursor: pointer" shadow="hover">
|
||||||
<div :inline="true">
|
<div :inline="true">
|
||||||
<div>
|
<div>
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
@ -79,7 +95,7 @@
|
|||||||
<el-input v-model="hostInfo.addr" style="width: 80%" />
|
<el-input v-model="hostInfo.addr" style="width: 80%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('terminal.port')" prop="port">
|
<el-form-item :label="$t('terminal.port')" prop="port">
|
||||||
<el-input v-model="hostInfo.port" style="width: 80%" />
|
<el-input v-model.number="hostInfo.port" style="width: 80%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('terminal.user')" prop="user">
|
<el-form-item :label="$t('terminal.user')" prop="user">
|
||||||
<el-input v-model="hostInfo.user" style="width: 80%" />
|
<el-input v-model="hostInfo.user" style="width: 80%" />
|
||||||
@ -90,13 +106,8 @@
|
|||||||
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
|
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item :label="$t('terminal.password')" v-if="hostInfo.authMode === 'password'" prop="password">
|
||||||
:label="$t('terminal.password')"
|
<el-input show-password type="password" v-model="hostInfo.password" style="width: 80%" />
|
||||||
show-password
|
|
||||||
v-if="hostInfo.authMode === 'password'"
|
|
||||||
prop="password"
|
|
||||||
>
|
|
||||||
<el-input type="password" v-model="hostInfo.password" style="width: 80%" />
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
|
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
|
||||||
<el-input type="textarea" v-model="hostInfo.privateKey" style="width: 80%" />
|
<el-input type="textarea" v-model="hostInfo.privateKey" style="width: 80%" />
|
||||||
@ -133,12 +144,13 @@ let timer: NodeJS.Timer | null = null;
|
|||||||
|
|
||||||
const terminalValue = ref();
|
const terminalValue = ref();
|
||||||
const terminalTabs = ref([]) as any;
|
const terminalTabs = ref([]) as any;
|
||||||
|
let tabIndex = 0;
|
||||||
const data = ref();
|
const data = ref();
|
||||||
const hostDrawer = ref(false);
|
const hostDrawer = ref(false);
|
||||||
|
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 10,
|
pageSize: 8,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,6 +187,9 @@ const handleTabsEdit = (targetName: string, action: 'remove' | 'add') => {
|
|||||||
if (action === 'add') {
|
if (action === 'add') {
|
||||||
connVisiable.value = true;
|
connVisiable.value = true;
|
||||||
operation.value = 'conn';
|
operation.value = 'conn';
|
||||||
|
if (hostInfoRef.value) {
|
||||||
|
hostInfoRef.value.resetFields();
|
||||||
|
}
|
||||||
} else if (action === 'remove') {
|
} else if (action === 'remove') {
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.refs[`Ref${targetName}`] && ctx.refs[`Ref${targetName}`][0].onClose();
|
ctx.refs[`Ref${targetName}`] && ctx.refs[`Ref${targetName}`][0].onClose();
|
||||||
@ -183,22 +198,23 @@ const handleTabsEdit = (targetName: string, action: 'remove' | 'add') => {
|
|||||||
let activeName = terminalValue.value;
|
let activeName = terminalValue.value;
|
||||||
if (activeName === targetName) {
|
if (activeName === targetName) {
|
||||||
tabs.forEach((tab: any, index: any) => {
|
tabs.forEach((tab: any, index: any) => {
|
||||||
if (tab.name === targetName) {
|
if (tab.key === targetName) {
|
||||||
const nextTab = tabs[index + 1] || tabs[index - 1];
|
const nextTab = tabs[index + 1] || tabs[index - 1];
|
||||||
if (nextTab) {
|
if (nextTab) {
|
||||||
activeName = nextTab.name;
|
activeName = nextTab.key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
terminalValue.value = activeName;
|
terminalValue.value = activeName;
|
||||||
terminalTabs.value = tabs.filter((tab: any) => tab.name !== targetName);
|
terminalTabs.value = tabs.filter((tab: any) => tab.key !== targetName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadHost = async () => {
|
const loadHost = async () => {
|
||||||
const res = await getHostList({ page: paginationConfig.currentPage, pageSize: paginationConfig.pageSize });
|
const res = await getHostList({ page: paginationConfig.currentPage, pageSize: paginationConfig.pageSize });
|
||||||
data.value = res.data.items;
|
data.value = res.data.items;
|
||||||
|
paginationConfig.total = res.data.total;
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
@ -247,17 +263,15 @@ const submitAddHost = (formEl: FormInstance | undefined) => {
|
|||||||
case 'conn':
|
case 'conn':
|
||||||
const res = await addHost(hostInfo);
|
const res = await addHost(hostInfo);
|
||||||
terminalTabs.value.push({
|
terminalTabs.value.push({
|
||||||
name: res.data.addr,
|
key: `${res.data.addr}-${++tabIndex}`,
|
||||||
title: res.data.addr,
|
title: res.data.addr,
|
||||||
wsID: res.data.id,
|
wsID: res.data.id,
|
||||||
type: 'remote',
|
|
||||||
status: 'online',
|
status: 'online',
|
||||||
});
|
});
|
||||||
terminalValue.value = res.data.addr;
|
terminalValue.value = `${res.data.addr}-${tabIndex}`;
|
||||||
}
|
}
|
||||||
connVisiable.value = false;
|
connVisiable.value = false;
|
||||||
loadHost();
|
loadHost();
|
||||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.success(i18n.global.t('commons.msg.loginSuccess') + ':' + error);
|
ElMessage.success(i18n.global.t('commons.msg.loginSuccess') + ':' + error);
|
||||||
}
|
}
|
||||||
@ -266,13 +280,23 @@ const submitAddHost = (formEl: FormInstance | undefined) => {
|
|||||||
|
|
||||||
const onConn = (row: Host.Host) => {
|
const onConn = (row: Host.Host) => {
|
||||||
terminalTabs.value.push({
|
terminalTabs.value.push({
|
||||||
name: row.addr,
|
key: `${row.addr}-${++tabIndex}`,
|
||||||
title: row.addr,
|
title: row.addr,
|
||||||
wsID: row.id,
|
wsID: row.id,
|
||||||
type: 'remote',
|
|
||||||
status: 'online',
|
status: 'online',
|
||||||
});
|
});
|
||||||
terminalValue.value = row.addr;
|
terminalValue.value = `${row.addr}-${tabIndex}`;
|
||||||
|
hostDrawer.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConnLocal = () => {
|
||||||
|
terminalTabs.value.push({
|
||||||
|
key: `127.0.0.1-${++tabIndex}`,
|
||||||
|
title: '127.0.0.1',
|
||||||
|
wsID: 0,
|
||||||
|
status: 'online',
|
||||||
|
});
|
||||||
|
terminalValue.value = `127.0.0.1-${tabIndex}`;
|
||||||
hostDrawer.value = false;
|
hostDrawer.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -291,24 +315,14 @@ function changeFrameHeight() {
|
|||||||
|
|
||||||
function syncTerminal() {
|
function syncTerminal() {
|
||||||
for (const terminal of terminalTabs.value) {
|
for (const terminal of terminalTabs.value) {
|
||||||
if (terminal.type === 'remote') {
|
if (ctx && ctx.refs[`Ref${terminal.key}`]) {
|
||||||
if (ctx && ctx.refs[`Ref${terminal.name}`]) {
|
terminal.status = ctx.refs[`Ref${terminal.key}`][0].isWsOpen() ? 'online' : 'closed';
|
||||||
terminal.status = ctx.refs[`Ref${terminal.name}`][0].isWsOpen() ? 'online' : 'closed';
|
|
||||||
console.log(terminal.status);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
terminalTabs.value.push({
|
onConnLocal();
|
||||||
name: '127.0.0.1',
|
|
||||||
title: '127.0.0.1',
|
|
||||||
src: 'http://localhost:8080',
|
|
||||||
type: 'local',
|
|
||||||
status: 'online',
|
|
||||||
});
|
|
||||||
terminalValue.value = '127.0.0.1';
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
changeFrameHeight();
|
changeFrameHeight();
|
||||||
window.addEventListener('resize', changeFrameHeight);
|
window.addEventListener('resize', changeFrameHeight);
|
||||||
@ -341,27 +355,27 @@ onBeforeMount(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.el-tabs {
|
.el-tabs {
|
||||||
::v-deep .el-tabs__header {
|
:deep .el-tabs__header {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 0 3px 0;
|
margin: 0 0 3px 0;
|
||||||
}
|
}
|
||||||
::v-deep .el-tabs__nav {
|
:deep .el-tabs__nav {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: transform var(--el-transition-duration);
|
transition: transform var(--el-transition-duration);
|
||||||
float: left;
|
float: left;
|
||||||
z-index: calc(var(--el-index-normal) + 1);
|
z-index: calc(var(--el-index-normal) + 1);
|
||||||
}
|
}
|
||||||
::v-deep .el-tabs__item {
|
:deep .el-tabs__item {
|
||||||
color: #575758;
|
color: #575758;
|
||||||
padding: 0 0px;
|
padding: 0 0px;
|
||||||
}
|
}
|
||||||
::v-deep .el-tabs__item.is-active {
|
:deep .el-tabs__item.is-active {
|
||||||
color: #ebeef5;
|
color: #ebeef5;
|
||||||
background-color: #575758;
|
background-color: #575758;
|
||||||
}
|
}
|
||||||
::v-deep .el-tabs__new-tab {
|
:deep .el-tabs__new-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="'terminal' + props.id"></div>
|
<div :id="'terminal' + props.terminalID"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -8,13 +8,17 @@ import { Terminal } from 'xterm';
|
|||||||
import { AttachAddon } from 'xterm-addon-attach';
|
import { AttachAddon } from 'xterm-addon-attach';
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import 'xterm/css/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
|
||||||
interface WsProps {
|
interface WsProps {
|
||||||
id: number;
|
terminalID: string;
|
||||||
|
wsID: number;
|
||||||
}
|
}
|
||||||
const props = withDefaults(defineProps<WsProps>(), {
|
const props = withDefaults(defineProps<WsProps>(), {
|
||||||
id: 0,
|
terminalID: '',
|
||||||
|
wsID: 0,
|
||||||
});
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
let terminalSocket = ref(null) as unknown as WebSocket;
|
let terminalSocket = ref(null) as unknown as WebSocket;
|
||||||
let term = ref(null) as unknown as Terminal;
|
let term = ref(null) as unknown as Terminal;
|
||||||
@ -54,7 +58,7 @@ const closeRealTerminal = (ev: CloseEvent) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initTerm = () => {
|
const initTerm = () => {
|
||||||
let ifm = document.getElementById('terminal' + props.id) as HTMLInputElement | null;
|
let ifm = document.getElementById('terminal' + props.terminalID) as HTMLInputElement | null;
|
||||||
term = new Terminal({
|
term = new Terminal({
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@ -66,15 +70,19 @@ const initTerm = () => {
|
|||||||
cursorStyle: 'underline',
|
cursorStyle: 'underline',
|
||||||
scrollback: 100,
|
scrollback: 100,
|
||||||
tabStopWidth: 4,
|
tabStopWidth: 4,
|
||||||
cols: ifm ? Math.floor(document.documentElement.clientWidth / 7) : 200,
|
|
||||||
rows: ifm ? Math.floor(document.documentElement.clientHeight / 20) : 25,
|
|
||||||
});
|
});
|
||||||
if (ifm) {
|
if (ifm) {
|
||||||
term.open(ifm);
|
term.open(ifm);
|
||||||
term.write('\n');
|
term.write('\n');
|
||||||
|
if (props.wsID === 0) {
|
||||||
terminalSocket = new WebSocket(
|
terminalSocket = new WebSocket(
|
||||||
`ws://localhost:9999/api/v1/terminals?id=${props.id}&cols=${term.cols}&rows=${term.rows}`,
|
`ws://localhost:9999/api/v1/terminals/local?cols=${term.cols}&rows=${term.rows}`,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
terminalSocket = new WebSocket(
|
||||||
|
`ws://localhost:9999/api/v1/terminals?id=${props.wsID}&cols=${term.cols}&rows=${term.rows}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
terminalSocket.onopen = runRealTerminal;
|
terminalSocket.onopen = runRealTerminal;
|
||||||
terminalSocket.onmessage = onWSReceive;
|
terminalSocket.onmessage = onWSReceive;
|
||||||
terminalSocket.onclose = closeRealTerminal;
|
terminalSocket.onclose = closeRealTerminal;
|
||||||
@ -90,7 +98,24 @@ const initTerm = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
term.loadAddon(new AttachAddon(terminalSocket));
|
term.loadAddon(new AttachAddon(terminalSocket));
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: term.cols,
|
||||||
|
rows: term.rows,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}, 30);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitTerm = () => {
|
||||||
|
fitAddon.fit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWsOpen = () => {
|
const isWsOpen = () => {
|
||||||
@ -105,20 +130,18 @@ function onClose() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeTerminalSize() {
|
function changeTerminalSize() {
|
||||||
let ifm = document.getElementById('terminal' + props.id) as HTMLInputElement | null;
|
fitTerm();
|
||||||
if (ifm) {
|
const { cols, rows } = term;
|
||||||
ifm.style.height = document.documentElement.clientHeight - 300 + 'px';
|
|
||||||
if (isWsOpen()) {
|
if (isWsOpen()) {
|
||||||
terminalSocket.send(
|
terminalSocket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'resize',
|
type: 'resize',
|
||||||
cols: Math.floor(document.documentElement.clientWidth / 7),
|
cols: cols,
|
||||||
rows: Math.floor(document.documentElement.clientHeight / 20),
|
rows: rows,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
onClose,
|
onClose,
|
||||||
@ -128,7 +151,6 @@ defineExpose({
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initTerm();
|
initTerm();
|
||||||
changeTerminalSize();
|
|
||||||
window.addEventListener('resize', changeTerminalSize);
|
window.addEventListener('resize', changeTerminalSize);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user