fix: 对于 Redis 终端、容器终端也同步改造

This commit is contained in:
Wankko Ree 2023-04-06 16:06:09 +08:00 committed by zhengkunwang223
parent fa83199d7b
commit 74b6af64e9
7 changed files with 82 additions and 292 deletions

View File

@ -1,6 +1,8 @@
package v1
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"
@ -196,7 +198,15 @@ func wshandleError(ws *websocket.Conn, err error) bool {
global.LOG.Errorf("handler ws faled:, err: %v", err)
dt := time.Now().Add(time.Second)
if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil {
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
wsData, err := json.Marshal(terminal.WsMsg{
Type: terminal.WsMsgCmd,
Data: base64.StdEncoding.EncodeToString([]byte(err.Error())),
})
if err != nil {
_ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}"))
} else {
_ = ws.WriteMessage(websocket.TextMessage, wsData)
}
}
return true
}

View File

@ -52,11 +52,17 @@ func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) {
func (sws *LocalWsSession) masterWrite(data []byte) error {
sws.writeMutex.Lock()
defer sws.writeMutex.Unlock()
err := sws.wsConn.WriteMessage(websocket.TextMessage, data)
wsData, err := json.Marshal(WsMsg{
Type: WsMsgCmd,
Data: base64.StdEncoding.EncodeToString(data),
})
if err != nil {
return errors.Wrapf(err, "failed to encoding to json")
}
err = sws.wsConn.WriteMessage(websocket.TextMessage, wsData)
if err != nil {
return errors.Wrapf(err, "failed to write to master")
}
return nil
}
@ -74,21 +80,27 @@ func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
global.LOG.Errorf("reading webSocket message failed, err: %v", err)
return
}
msgObj := wsMsg{}
msgObj := WsMsg{}
_ = json.Unmarshal(wsData, &msgObj)
switch msgObj.Type {
case wsMsgResize:
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:
case WsMsgCmd:
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data)
if err != nil {
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
case WsMsgHeartbeat:
// 接收到心跳包后将心跳包原样返回,可以用于网络延迟检测等情况
err = wsConn.WriteMessage(websocket.TextMessage, wsData)
if err != nil {
global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err)
}
}
}
}

View File

@ -35,17 +35,17 @@ func (w *safeBuffer) Reset() {
}
const (
wsMsgCmd = "cmd"
wsMsgResize = "resize"
wsMsgHeartbeat = "heartbeat"
WsMsgCmd = "cmd"
WsMsgResize = "resize"
WsMsgHeartbeat = "heartbeat"
)
type wsMsg struct {
type WsMsg struct {
Type string `json:"type"`
Data string `json:"data,omitempty"` // wsMsgCmd
Cols int `json:"cols,omitempty"` // wsMsgResize
Rows int `json:"rows,omitempty"` // wsMsgResize
Timestamp int `json:"timestamp,omitempty"` // wsMsgHeartbeat
Data string `json:"data,omitempty"` // WsMsgCmd
Cols int `json:"cols,omitempty"` // WsMsgResize
Rows int `json:"rows,omitempty"` // WsMsgResize
Timestamp int `json:"timestamp,omitempty"` // WsMsgHeartbeat
}
type LogicSshWsSession struct {
@ -127,22 +127,22 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
if err != nil {
return
}
msgObj := wsMsg{}
msgObj := WsMsg{}
_ = json.Unmarshal(wsData, &msgObj)
switch msgObj.Type {
case wsMsgResize:
case WsMsgResize:
if msgObj.Cols > 0 && msgObj.Rows > 0 {
if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
global.LOG.Errorf("ssh pty change windows size failed, err: %v", err)
}
}
case wsMsgCmd:
case WsMsgCmd:
decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data)
if err != nil {
global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err)
}
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
case wsMsgHeartbeat:
case WsMsgHeartbeat:
// 接收到心跳包后将心跳包原样返回,可以用于网络延迟检测等情况
err = wsConn.WriteMessage(websocket.TextMessage, wsData)
if err != nil {
@ -173,8 +173,8 @@ func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) {
}
bs := sws.comboOutput.Bytes()
if len(bs) > 0 {
wsData, err := json.Marshal(wsMsg{
Type: wsMsgCmd,
wsData, err := json.Marshal(WsMsg{
Type: WsMsgCmd,
Data: base64.StdEncoding.EncodeToString(bs),
})
if err != nil {

View File

@ -68,14 +68,16 @@ const initError = (errorInfo: string) => {
}
};
function onClose() {
function onClose(isKeepShow: boolean = false) {
window.removeEventListener('resize', changeTerminalSize);
try {
terminalSocket.value?.close();
} catch {}
try {
term.value.dispose();
} catch {}
if (!isKeepShow) {
try {
term.value.dispose();
} catch {}
}
}
// terminal start
@ -162,7 +164,7 @@ const onWSReceive = (message: MessageEvent) => {
switch (wsMsg.type) {
case 'cmd': {
term.value.element && term.value.focus();
term.value.write(Base64.decode(wsMsg.data));
wsMsg.data && term.value.write(Base64.decode(wsMsg.data)); // RedisCtrAliveexittodo
break;
}
case 'heartbeat': {

View File

@ -44,7 +44,7 @@
{{ $t('commons.button.conn') }}
</el-button>
<el-button v-else @click="handleClose()">{{ $t('commons.button.disconn') }}</el-button>
<div style="height: calc(100vh - 302px)" :id="'terminal-exec'"></div>
<Terminal style="height: calc(100vh - 302px)" ref="terminalRef"></Terminal>
</el-form>
</el-drawer>
</template>
@ -52,24 +52,12 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { ElForm, FormInstance } from 'element-plus';
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';
import { Rules } from '@/global/form-rules';
import { isJson } from '@/utils/util';
import Terminal from '@/components/terminal/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue';
const terminalVisiable = ref(false);
const terminalOpen = ref(false);
const fitAddon = new FitAddon();
let terminalSocket = ref(null) as unknown as WebSocket;
let term = ref(null) as unknown as Terminal;
const loading = ref(true);
const runRealTerminal = () => {
loading.value = false;
};
const form = reactive({
isCustom: false,
command: '',
@ -78,6 +66,7 @@ const form = reactive({
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
interface DialogProps {
containerID: string;
@ -89,125 +78,31 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
form.user = '';
form.command = '/bin/bash';
terminalOpen.value = false;
window.addEventListener('resize', changeTerminalSize);
};
const onChangeCommand = async () => {
form.command = '';
};
const onWSReceive = (message: any) => {
if (!isJson(message.data)) {
return;
}
const data = JSON.parse(message.data);
term.element && term.focus();
term.write(data.Data);
};
const errorRealTerminal = (ex: any) => {
let message = ex.message;
if (!message) message = 'disconnected';
term.write(`\x1b[31m${message}\x1b[m\r\n`);
};
const closeRealTerminal = (ev: CloseEvent) => {
term.write(ev.reason);
};
const initTerm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let href = window.location.href;
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
let ipLocal = href.split('//')[1].split('/')[0];
terminalOpen.value = true;
let ifm = document.getElementById('terminal-exec') as HTMLInputElement | null;
term = new Terminal({
lineHeight: 1.2,
fontSize: 12,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#000000',
},
cursorBlink: true,
cursorStyle: 'underline',
scrollback: 100,
tabStopWidth: 4,
terminalRef.value!.acceptParams({
endpoint: '/api/v1/containers/exec',
args: `containerid=${form.containerID}&user=${form.user}&command=${form.command}`,
error: '',
});
if (ifm) {
term.open(ifm);
terminalSocket = new WebSocket(
`${protocol}://${ipLocal}/api/v1/containers/exec?containerid=${form.containerID}&cols=${term.cols}&rows=${term.rows}&user=${form.user}&command=${form.command}`,
);
terminalSocket.onopen = runRealTerminal;
terminalSocket.onmessage = onWSReceive;
terminalSocket.onclose = closeRealTerminal;
terminalSocket.onerror = errorRealTerminal;
term.onData((data: any) => {
if (isWsOpen()) {
terminalSocket.send(
JSON.stringify({
type: 'cmd',
cmd: Base64.encode(data),
}),
);
}
});
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;
if (readyState) {
return readyState === 1;
}
return false;
};
function handleClose() {
window.removeEventListener('resize', changeTerminalSize);
if (isWsOpen()) {
terminalSocket && terminalSocket.close();
term.dispose();
}
terminalRef.value?.onClose();
terminalVisiable.value = false;
terminalOpen.value = false;
}
function changeTerminalSize() {
fitTerm();
const { cols, rows } = term;
if (isWsOpen()) {
terminalSocket.send(
JSON.stringify({
type: 'resize',
cols: cols,
rows: rows,
}),
);
}
}
defineExpose({
acceptParams,
});

View File

@ -20,7 +20,12 @@
</div>
</template>
<template #main v-if="redisIsExist && !isOnSetting">
<Terminal :key="isRefresh" ref="terminalRef" />
<Terminal
style="height: calc(100vh - 370px)"
:key="isRefresh"
ref="terminalRef"
v-show="terminalShow"
/>
</template>
</LayoutContent>
@ -55,7 +60,7 @@
import LayoutContent from '@/layout/layout-content.vue';
import Setting from '@/views/database/redis/setting/index.vue';
import Password from '@/views/database/redis/password/index.vue';
import Terminal from '@/views/database/redis/terminal/index.vue';
import Terminal from '@/components/terminal/index.vue';
import AppStatus from '@/components/app-status/index.vue';
import { nextTick, onBeforeUnmount, ref } from 'vue';
import { App } from '@/api/interface/app';
@ -65,12 +70,13 @@ import router from '@/routers';
const loading = ref(false);
const maskShow = ref(true);
const terminalRef = ref();
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
const settingRef = ref();
const isOnSetting = ref(false);
const redisIsExist = ref(false);
const redisStatus = ref();
const redisName = ref();
const terminalShow = ref(false);
const redisCommandPort = ref();
const commandVisiable = ref(false);
@ -80,6 +86,7 @@ const isRefresh = ref();
const onSetting = async () => {
isOnSetting.value = true;
terminalRef.value?.onClose(false);
terminalShow.value = false;
settingRef.value!.acceptParams({ status: redisStatus.value, redisName: redisName.value });
};
@ -114,19 +121,32 @@ const checkExist = (data: App.CheckInstalled) => {
if (redisStatus.value === 'Running') {
loadDashboardPort();
nextTick(() => {
terminalRef.value.acceptParams();
terminalShow.value = true;
terminalRef.value.acceptParams({
endpoint: '/api/v1/databases/redis/exec',
args: '',
error: '',
});
});
}
};
const initTerminal = async () => {
if (redisStatus.value === 'Running') {
terminalRef.value.acceptParams();
nextTick(() => {
terminalShow.value = true;
terminalRef.value.acceptParams({
endpoint: '/api/v1/databases/redis/exec',
args: '',
error: '',
});
});
}
};
const closeTerminal = async (isKeepShow: boolean) => {
isRefresh.value = !isRefresh.value;
terminalRef.value?.onClose(isKeepShow);
terminalShow.value = isKeepShow;
};
const onBefore = () => {

View File

@ -1,149 +0,0 @@
<template>
<div v-show="terminalShow" style="height: 100%">
<div style="height: calc(100vh - 370px)" :id="'terminal-exec'"></div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
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';
import { isJson } from '@/utils/util';
const fitAddon = new FitAddon();
let terminalSocket = ref(null) as unknown as WebSocket;
let term = ref(null) as unknown as Terminal;
const loading = ref(true);
const runRealTerminal = () => {
loading.value = false;
};
const terminalShow = ref(false);
const acceptParams = async (): Promise<void> => {
terminalShow.value = true;
initTerm();
window.addEventListener('resize', changeTerminalSize);
};
const onClose = async (isKeepShow: boolean) => {
window.removeEventListener('resize', changeTerminalSize);
if (isWsOpen()) {
terminalSocket && terminalSocket.close();
}
if (!isKeepShow) {
term.dispose();
}
terminalShow.value = isKeepShow;
};
const onWSReceive = (message: any) => {
if (!isJson(message.data)) {
return;
}
const data = JSON.parse(message.data);
term.element && term.focus();
term.write(data.Data);
};
const errorRealTerminal = (ex: any) => {
let message = ex.message;
if (!message) message = 'disconnected';
term.write(`\x1b[31m${message}\x1b[m\r\n`);
};
const closeRealTerminal = (ev: CloseEvent) => {
term.write(ev.reason);
};
const initTerm = () => {
let ifm = document.getElementById('terminal-exec') as HTMLInputElement | null;
let href = window.location.href;
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
let ipLocal = href.split('//')[1].split('/')[0];
term = new Terminal({
lineHeight: 1.2,
fontSize: 12,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#000000',
},
cursorBlink: true,
cursorStyle: 'underline',
scrollback: 100,
tabStopWidth: 4,
});
if (ifm) {
term.open(ifm);
terminalSocket = new WebSocket(
`${protocol}://${ipLocal}/api/v1/databases/redis/exec?cols=${term.cols}&rows=${term.rows}`,
);
terminalSocket.onopen = runRealTerminal;
terminalSocket.onmessage = onWSReceive;
terminalSocket.onclose = closeRealTerminal;
terminalSocket.onerror = errorRealTerminal;
term.onData((data: any) => {
if (isWsOpen()) {
terminalSocket.send(
JSON.stringify({
type: 'cmd',
cmd: Base64.encode(data),
}),
);
}
});
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;
if (readyState) {
return readyState === 1;
}
return false;
};
function changeTerminalSize() {
fitTerm();
const { cols, rows } = term;
if (isWsOpen()) {
terminalSocket.send(
JSON.stringify({
type: 'resize',
cols: cols,
rows: rows,
}),
);
}
}
defineExpose({
acceptParams,
onClose,
});
</script>
<style lang="scss" scoped>
#terminal {
width: 100%;
height: 100%;
}
</style>