mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-27 20:49:03 +08:00
fix: 对于 Redis 终端、容器终端也同步改造
This commit is contained in:
parent
fa83199d7b
commit
74b6af64e9
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)); // 这里理论上不用判断,但是Redis和Ctr还没实现Alive处理,所以exit后会一直发数据,todo
|
||||
break;
|
||||
}
|
||||
case 'heartbeat': {
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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 = () => {
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user