mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-23 18:49:21 +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 {
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handler ws faled:, err: %v", err)
|
||||
|
@ -3,7 +3,7 @@ package dto
|
||||
import "time"
|
||||
|
||||
type HostCreate struct {
|
||||
Name string `json:"name" validate:"required,name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Addr string `json:"addr" validate:"required,ip"`
|
||||
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
||||
User string `json:"user" validate:"required"`
|
||||
@ -27,7 +27,7 @@ type HostInfo struct {
|
||||
}
|
||||
|
||||
type HostUpdate struct {
|
||||
Name string `json:"name" validate:"required,name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Addr string `json:"addr" validate:"required,ip"`
|
||||
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
||||
User string `json:"user" validate:"required"`
|
||||
|
@ -15,6 +15,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gwatts/gin-adapter v1.0.0
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/kr/pty v1.1.1
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
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.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -16,17 +18,31 @@ import (
|
||||
func OperationRecord() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
body, err = ioutil.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("read body from request failed, err: %v", err)
|
||||
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 {
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
var err error
|
||||
body, err = ioutil.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("read body from request failed, err: %v", err)
|
||||
} else {
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
}
|
||||
pathInfo := loadLogInfo(c.Request.URL.Path)
|
||||
|
||||
|
@ -2,7 +2,6 @@ package router
|
||||
|
||||
import (
|
||||
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
||||
"github.com/1Panel-dev/1Panel/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -11,9 +10,9 @@ type TerminalRouter struct{}
|
||||
|
||||
func (s *UserRouter) InitTerminalRouter(Router *gin.RouterGroup) {
|
||||
terminalRouter := Router.Group("terminals")
|
||||
withRecordRouter := terminalRouter.Use(middleware.OperationRecord())
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
withRecordRouter.GET("", baseApi.WsSsh)
|
||||
terminalRouter.GET("", baseApi.WsSsh)
|
||||
terminalRouter.GET("/local", baseApi.LocalWsSsh)
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ package server
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/init/cache"
|
||||
"github.com/1Panel-dev/1Panel/init/session"
|
||||
"github.com/1Panel-dev/1Panel/init/session/psession"
|
||||
"time"
|
||||
|
||||
"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/log"
|
||||
"github.com/1Panel-dev/1Panel/init/migration"
|
||||
@ -30,7 +30,6 @@ func Start() {
|
||||
gob.Register(psession.SessionUser{})
|
||||
cache.Init()
|
||||
session.Init()
|
||||
binary.StartTTY()
|
||||
gin.SetMode(global.CONF.System.Level)
|
||||
|
||||
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) {
|
||||
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",
|
||||
"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": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
@ -40,7 +40,8 @@
|
||||
"vue-router": "^4.0.12",
|
||||
"vue3-seamless-scroll": "^1.2.0",
|
||||
"xterm": "^4.19.0",
|
||||
"xterm-addon-attach": "^0.6.0"
|
||||
"xterm-addon-attach": "^0.6.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.1",
|
||||
|
@ -92,6 +92,7 @@ export default {
|
||||
connHistory: 'historys',
|
||||
hostHistory: 'History record',
|
||||
addHost: 'Add Host',
|
||||
localhost: 'Localhost',
|
||||
name: 'Name',
|
||||
port: 'Port',
|
||||
user: 'User',
|
||||
@ -100,6 +101,7 @@ export default {
|
||||
keyMode: 'PrivateKey',
|
||||
password: 'Password',
|
||||
key: 'Private Key',
|
||||
emptyTerminal: 'No terminal is currently connected',
|
||||
},
|
||||
operations: {
|
||||
detail: {
|
||||
|
@ -93,6 +93,7 @@ export default {
|
||||
connHistory: '历史连接',
|
||||
hostHistory: '历史主机信息',
|
||||
addHost: '添加主机',
|
||||
localhost: '本地服务器',
|
||||
name: '名称',
|
||||
port: '端口',
|
||||
user: '用户',
|
||||
@ -101,6 +102,7 @@ export default {
|
||||
keyMode: '密钥输入',
|
||||
password: '密码',
|
||||
key: '密钥',
|
||||
emptyTerminal: '暂无终端连接',
|
||||
},
|
||||
operations: {
|
||||
detail: {
|
||||
|
@ -12,7 +12,7 @@
|
||||
v-model="terminalValue"
|
||||
@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>
|
||||
<span class="custom-tabs-label">
|
||||
<el-icon color="#67C23A" v-if="item.status === 'online'"><circleCheck /></el-icon>
|
||||
@ -20,24 +20,40 @@
|
||||
<span> {{ item.title }} </span>
|
||||
</span>
|
||||
</template>
|
||||
<iframe
|
||||
v-if="item.type === 'local'"
|
||||
id="iframeTerminal"
|
||||
name="iframeTerminal"
|
||||
width="100%"
|
||||
frameborder="0"
|
||||
:src="item.src"
|
||||
/>
|
||||
<Terminal v-else :ref="'Ref' + item.name" :id="item.wsID"></Terminal>
|
||||
<Terminal
|
||||
style="height: calc(100vh - 265px); background-color: #000"
|
||||
:ref="'Ref' + item.key"
|
||||
:wsID="item.wsID"
|
||||
:terminalID="item.key"
|
||||
></Terminal>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<el-drawer :size="320" v-model="hostDrawer" :title="$t('terminal.hostHistory')" direction="rtl">
|
||||
<el-button @click="onAddHost">{{ $t('terminal.addHost') }}</el-button>
|
||||
<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">
|
||||
<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>
|
||||
<span>{{ item.name }}</span>
|
||||
@ -79,7 +95,7 @@
|
||||
<el-input v-model="hostInfo.addr" style="width: 80%" />
|
||||
</el-form-item>
|
||||
<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 :label="$t('terminal.user')" prop="user">
|
||||
<el-input v-model="hostInfo.user" style="width: 80%" />
|
||||
@ -90,13 +106,8 @@
|
||||
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('terminal.password')"
|
||||
show-password
|
||||
v-if="hostInfo.authMode === 'password'"
|
||||
prop="password"
|
||||
>
|
||||
<el-input type="password" v-model="hostInfo.password" style="width: 80%" />
|
||||
<el-form-item :label="$t('terminal.password')" v-if="hostInfo.authMode === 'password'" prop="password">
|
||||
<el-input show-password type="password" v-model="hostInfo.password" style="width: 80%" />
|
||||
</el-form-item>
|
||||
<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%" />
|
||||
@ -133,12 +144,13 @@ let timer: NodeJS.Timer | null = null;
|
||||
|
||||
const terminalValue = ref();
|
||||
const terminalTabs = ref([]) as any;
|
||||
let tabIndex = 0;
|
||||
const data = ref();
|
||||
const hostDrawer = ref(false);
|
||||
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 8,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
@ -175,6 +187,9 @@ const handleTabsEdit = (targetName: string, action: 'remove' | 'add') => {
|
||||
if (action === 'add') {
|
||||
connVisiable.value = true;
|
||||
operation.value = 'conn';
|
||||
if (hostInfoRef.value) {
|
||||
hostInfoRef.value.resetFields();
|
||||
}
|
||||
} else if (action === 'remove') {
|
||||
if (ctx) {
|
||||
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;
|
||||
if (activeName === targetName) {
|
||||
tabs.forEach((tab: any, index: any) => {
|
||||
if (tab.name === targetName) {
|
||||
if (tab.key === targetName) {
|
||||
const nextTab = tabs[index + 1] || tabs[index - 1];
|
||||
if (nextTab) {
|
||||
activeName = nextTab.name;
|
||||
activeName = nextTab.key;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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 res = await getHostList({ page: paginationConfig.currentPage, pageSize: paginationConfig.pageSize });
|
||||
data.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
@ -247,17 +263,15 @@ const submitAddHost = (formEl: FormInstance | undefined) => {
|
||||
case 'conn':
|
||||
const res = await addHost(hostInfo);
|
||||
terminalTabs.value.push({
|
||||
name: res.data.addr,
|
||||
key: `${res.data.addr}-${++tabIndex}`,
|
||||
title: res.data.addr,
|
||||
wsID: res.data.id,
|
||||
type: 'remote',
|
||||
status: 'online',
|
||||
});
|
||||
terminalValue.value = res.data.addr;
|
||||
terminalValue.value = `${res.data.addr}-${tabIndex}`;
|
||||
}
|
||||
connVisiable.value = false;
|
||||
loadHost();
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
} catch (error) {
|
||||
ElMessage.success(i18n.global.t('commons.msg.loginSuccess') + ':' + error);
|
||||
}
|
||||
@ -266,13 +280,23 @@ const submitAddHost = (formEl: FormInstance | undefined) => {
|
||||
|
||||
const onConn = (row: Host.Host) => {
|
||||
terminalTabs.value.push({
|
||||
name: row.addr,
|
||||
key: `${row.addr}-${++tabIndex}`,
|
||||
title: row.addr,
|
||||
wsID: row.id,
|
||||
type: 'remote',
|
||||
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;
|
||||
};
|
||||
|
||||
@ -291,24 +315,14 @@ function changeFrameHeight() {
|
||||
|
||||
function syncTerminal() {
|
||||
for (const terminal of terminalTabs.value) {
|
||||
if (terminal.type === 'remote') {
|
||||
if (ctx && ctx.refs[`Ref${terminal.name}`]) {
|
||||
terminal.status = ctx.refs[`Ref${terminal.name}`][0].isWsOpen() ? 'online' : 'closed';
|
||||
console.log(terminal.status);
|
||||
}
|
||||
if (ctx && ctx.refs[`Ref${terminal.key}`]) {
|
||||
terminal.status = ctx.refs[`Ref${terminal.key}`][0].isWsOpen() ? 'online' : 'closed';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
terminalTabs.value.push({
|
||||
name: '127.0.0.1',
|
||||
title: '127.0.0.1',
|
||||
src: 'http://localhost:8080',
|
||||
type: 'local',
|
||||
status: 'online',
|
||||
});
|
||||
terminalValue.value = '127.0.0.1';
|
||||
onConnLocal();
|
||||
nextTick(() => {
|
||||
changeFrameHeight();
|
||||
window.addEventListener('resize', changeFrameHeight);
|
||||
@ -341,27 +355,27 @@ onBeforeMount(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-tabs {
|
||||
::v-deep .el-tabs__header {
|
||||
:deep .el-tabs__header {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
margin: 0 0 3px 0;
|
||||
}
|
||||
::v-deep .el-tabs__nav {
|
||||
:deep .el-tabs__nav {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
transition: transform var(--el-transition-duration);
|
||||
float: left;
|
||||
z-index: calc(var(--el-index-normal) + 1);
|
||||
}
|
||||
::v-deep .el-tabs__item {
|
||||
:deep .el-tabs__item {
|
||||
color: #575758;
|
||||
padding: 0 0px;
|
||||
}
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
:deep .el-tabs__item.is-active {
|
||||
color: #ebeef5;
|
||||
background-color: #575758;
|
||||
}
|
||||
::v-deep .el-tabs__new-tab {
|
||||
:deep .el-tabs__new-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :id="'terminal' + props.id"></div>
|
||||
<div :id="'terminal' + props.terminalID"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -8,13 +8,17 @@ import { Terminal } from 'xterm';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
import { Base64 } from 'js-base64';
|
||||
import 'xterm/css/xterm.css';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
|
||||
interface WsProps {
|
||||
id: number;
|
||||
terminalID: string;
|
||||
wsID: number;
|
||||
}
|
||||
const props = withDefaults(defineProps<WsProps>(), {
|
||||
id: 0,
|
||||
terminalID: '',
|
||||
wsID: 0,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
const loading = ref(true);
|
||||
let terminalSocket = ref(null) as unknown as WebSocket;
|
||||
let term = ref(null) as unknown as Terminal;
|
||||
@ -54,7 +58,7 @@ const closeRealTerminal = (ev: CloseEvent) => {
|
||||
};
|
||||
|
||||
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({
|
||||
lineHeight: 1.2,
|
||||
fontSize: 12,
|
||||
@ -66,15 +70,19 @@ const initTerm = () => {
|
||||
cursorStyle: 'underline',
|
||||
scrollback: 100,
|
||||
tabStopWidth: 4,
|
||||
cols: ifm ? Math.floor(document.documentElement.clientWidth / 7) : 200,
|
||||
rows: ifm ? Math.floor(document.documentElement.clientHeight / 20) : 25,
|
||||
});
|
||||
if (ifm) {
|
||||
term.open(ifm);
|
||||
term.write('\n');
|
||||
terminalSocket = new WebSocket(
|
||||
`ws://localhost:9999/api/v1/terminals?id=${props.id}&cols=${term.cols}&rows=${term.rows}`,
|
||||
);
|
||||
if (props.wsID === 0) {
|
||||
terminalSocket = new WebSocket(
|
||||
`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.onmessage = onWSReceive;
|
||||
terminalSocket.onclose = closeRealTerminal;
|
||||
@ -90,9 +98,26 @@ const initTerm = () => {
|
||||
}
|
||||
});
|
||||
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 readyState = terminalSocket && terminalSocket.readyState;
|
||||
return readyState === 1;
|
||||
@ -105,18 +130,16 @@ function onClose() {
|
||||
}
|
||||
|
||||
function changeTerminalSize() {
|
||||
let ifm = document.getElementById('terminal' + props.id) as HTMLInputElement | null;
|
||||
if (ifm) {
|
||||
ifm.style.height = document.documentElement.clientHeight - 300 + 'px';
|
||||
if (isWsOpen()) {
|
||||
terminalSocket.send(
|
||||
JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: Math.floor(document.documentElement.clientWidth / 7),
|
||||
rows: Math.floor(document.documentElement.clientHeight / 20),
|
||||
}),
|
||||
);
|
||||
}
|
||||
fitTerm();
|
||||
const { cols, rows } = term;
|
||||
if (isWsOpen()) {
|
||||
terminalSocket.send(
|
||||
JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: cols,
|
||||
rows: rows,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,7 +151,6 @@ defineExpose({
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
initTerm();
|
||||
changeTerminalSize();
|
||||
window.addEventListener('resize', changeTerminalSize);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user