From 94097a1c6d16820088e4ba8397323b7ad67dea0a Mon Sep 17 00:00:00 2001 From: chaos-zhu Date: Mon, 14 Oct 2024 22:52:49 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E7=BB=88=E7=AB=AF=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E6=9C=8D=E5=8A=A1=E7=AB=AFping=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E5=BB=B6=E8=BF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++ server/app/socket/terminal.js | 12 +++- server/app/utils/tools.js | 45 ++++++++++++++- server/package.json | 1 + web/src/views/server/components/host-form.vue | 2 +- .../views/terminal/components/info-side.vue | 55 ++++++++++--------- .../terminal/components/terminal-tab.vue | 14 ++++- .../views/terminal/components/terminal.vue | 7 +++ yarn.lock | 9 ++- 9 files changed, 120 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d66aa..bf3a02d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2.2.7](https://github.com/chaos-zhu/easynode/releases) (2024-10-xx) + +### Features + +* 终端连接页新增展示服务端ping客户端延迟ms +* 修复自定义客户端端口默认字符串的bug + ## [2.2.6](https://github.com/chaos-zhu/easynode/releases) (2024-10-14) ### Features diff --git a/server/app/socket/terminal.js b/server/app/socket/terminal.js index 514dcdd..1bef4b9 100644 --- a/server/app/socket/terminal.js +++ b/server/app/socket/terminal.js @@ -4,7 +4,7 @@ const { verifyAuthSync } = require('../utils/verify-auth') const { AESDecryptSync } = require('../utils/encrypt') const { readSSHRecord, readHostList } = require('../utils/storage') const { asyncSendNotice } = require('../utils/notify') -const { isAllowedIp } = require('../utils/tools') +const { isAllowedIp, ping } = require('../utils/tools') function createInteractiveShell(socket, sshClient) { return new Promise((resolve) => { @@ -98,6 +98,7 @@ async function createTerminal(hostId, socket, sshClient) { ...authInfo // debug: (info) => console.log(info) }) + } catch (err) { consola.error('创建终端失败: ', host, err.message) socket.emit('create_fail', err.message) @@ -147,6 +148,7 @@ module.exports = (httpServer) => { } socket.on('input', listenerInput) socket.on('resize', resizeShell) + // 重连 socket.on('reconnect_terminal', async () => { consola.info('重连终端: ', hostId) @@ -168,6 +170,14 @@ module.exports = (httpServer) => { stream = await createTerminal(hostId, socket, sshClient) }) + socket.on('get_ping',async (ip) => { + try { + socket.emit('ping_data', await ping(ip, 2500)) + } catch (error) { + socket.emit('ping_data', { success: false, msg: error.message }) + } + }) + socket.on('disconnect', (reason) => { consola.info('终端socket连接断开:', reason) }) diff --git a/server/app/utils/tools.js b/server/app/utils/tools.js index 7ac3162..e190530 100644 --- a/server/app/utils/tools.js +++ b/server/app/utils/tools.js @@ -1,4 +1,7 @@ +const { exec } = require('child_process') +const os = require('os') const net = require('net') +const iconv = require('iconv-lite') const axios = require('axios') const request = axios.create({ timeout: 3000 }) @@ -240,6 +243,45 @@ const isAllowedIp = (requestIP) => { return flag } +const ping = (ip, timeout = 5000) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ success: false, msg: 'ping timeout!' }) + }, timeout) + let isWin = os.platform() === 'win32' + const command = isWin ? `ping -n 1 ${ ip }` : `ping -c 1 ${ ip }` + const options = isWin ? { encoding: 'buffer' } : {} + + exec(command, options, (error, stdout) => { + if (error) { + resolve({ success: false, msg: 'ping error!' }) + return + } + let output + if (isWin) { + output = iconv.decode(stdout, 'cp936') + } else { + output = stdout.toString() + } + // console.log('output:', output) + let match + if (isWin) { + match = output.match(/平均 = (\d+)ms/) + if (!match) { + match = output.match(/Average = (\d+)ms/) + } + } else { + match = output.match(/rtt min\/avg\/max\/mdev = [\d.]+\/([\d.]+)\/[\d.]+\/[\d.]+/) + } + if (match) { + resolve({ success: true, time: parseFloat(match[1]) }) + } else { + resolve({ success: false, msg: 'Could not find time in ping output!' }) + } + }) + }) +} + module.exports = { getNetIPInfo, throwError, @@ -250,5 +292,6 @@ module.exports = { resolvePath, shellThrottle, isProd, - isAllowedIp + isAllowedIp, + ping } \ No newline at end of file diff --git a/server/package.json b/server/package.json index 2894acd..d342646 100644 --- a/server/package.json +++ b/server/package.json @@ -28,6 +28,7 @@ "dotenv": "^16.4.5", "fs-extra": "^11.2.0", "global": "^4.4.0", + "iconv-lite": "^0.6.3", "jsonwebtoken": "^9.0.2", "koa": "^2.15.3", "koa-body": "^6.0.1", diff --git a/web/src/views/server/components/host-form.vue b/web/src/views/server/components/host-form.vue index 39a5c1f..895e3bf 100644 --- a/web/src/views/server/components/host-form.vue +++ b/web/src/views/server/components/host-form.vue @@ -299,7 +299,7 @@ const formField = { password: '', privateKey: '', credential: '', // credentials -> _id - clientPort: '22022', + clientPort: 22022, index: 0, expired: null, expiredNotify: false, diff --git a/web/src/views/terminal/components/info-side.vue b/web/src/views/terminal/components/info-side.vue index 767444b..6e3d0fd 100644 --- a/web/src/views/terminal/components/info-side.vue +++ b/web/src/views/terminal/components/info-side.vue @@ -14,7 +14,10 @@ {{ host }} - 复制 + + 复制 -
{{ ipInfo.country || '--' }} {{ ipInfo.regionName }}
- @@ -54,7 +56,7 @@ :text-inside="true" :stroke-width="18" :percentage="cpuUsage" - :color="handleColor(cpuUsage)" + :color="handleUsedColor(cpuUsage)" /> @@ -67,7 +69,7 @@ :text-inside="true" :stroke-width="18" :percentage="usedMemPercentage" - :color="handleColor(usedMemPercentage)" + :color="handleUsedColor(usedMemPercentage)" />
{{ $tools.toFixed(memInfo.usedMemMb / 1024) }}/{{ $tools.toFixed(memInfo.totalMemMb / 1024) }}G @@ -83,7 +85,7 @@ :text-inside="true" :stroke-width="18" :percentage="swapPercentage" - :color="handleColor(swapPercentage)" + :color="handleUsedColor(swapPercentage)" />
{{ $tools.toFixed(swapInfo.swapUsed / 1024) }}/{{ $tools.toFixed(swapInfo.swapTotal / 1024) }}G @@ -99,7 +101,7 @@ :text-inside="true" :stroke-width="18" :percentage="usedPercentage" - :color="handleColor(usedPercentage)" + :color="handleUsedColor(usedPercentage)" />
{{ driveInfo.usedGb || '--' }}/{{ driveInfo.totalGb || '--' }}G @@ -229,18 +231,18 @@ const props = defineProps({ showInputCommand: { required: true, type: Boolean + }, + pingData: { + required: true, + type: Object } }) const emit = defineEmits(['update:inputCommandStyle', 'connect-sftp', 'click-input-command',]) const socket = ref(null) -// const name = ref('') -const ping = ref(0) const pingTimer = ref(null) -// const sftpStatus = ref(false) -// const token = computed(() => $store.token) const hostData = computed(() => props.hostInfo.monitorData || {}) const host = computed(() => props.hostInfo.host) const ipInfo = computed(() => hostData.value?.ipInfo || {}) @@ -280,6 +282,12 @@ const inputCommandStyle = computed({ } }) +const pingMs = computed(() => { + let curPingData = props.pingData[host.value] || {} + if (!curPingData?.success) return false + return Number(curPingData?.time).toFixed(0) +}) + // const handleSftp = () => { // sftpStatus.value = !sftpStatus.value // emit('connect-sftp', sftpStatus.value) @@ -295,23 +303,17 @@ const handleCopy = async () => { $message.success({ message: 'success', center: true }) } -const handleColor = (num) => { +const handleUsedColor = (num) => { if (num < 60) return '#13ce66' if (num < 80) return '#e6a23c' if (num <= 100) return '#ff4949' } -// const getHostPing = () => { -// pingTimer.value = setInterval(() => { -// $tools.ping(`http://${ props.host }:22022`) -// .then(res => { -// ping.value = res -// if (!import.meta.env.DEV) { -// console.warn('Please tick \'Preserve Log\'') -// } -// }) -// }, 3000) -// } +const handlePingColor = (num) => { + if (num < 100) return 'rgba(19, 206, 102, 0.5)' // #13ce66 + if (num < 250) return 'rgba(230, 162, 60, 0.5)' // #e6a23c + return 'rgba(255, 73, 73, 0.5)' // #ff4949 +} onBeforeUnmount(() => { socket.value && socket.value.close() @@ -351,10 +353,9 @@ onBeforeUnmount(() => { .host-ping { display: inline-block; - font-size: 13px; - color: #009933; - background-color: #e8fff3; + font-size: 10px; padding: 0 5px; + border-radius: 2px; } // 分割线title diff --git a/web/src/views/terminal/components/terminal-tab.vue b/web/src/views/terminal/components/terminal-tab.vue index 8f96c08..4a91b8c 100644 --- a/web/src/views/terminal/components/terminal-tab.vue +++ b/web/src/views/terminal/components/terminal-tab.vue @@ -48,13 +48,14 @@ const props = defineProps({ } }) -const emit = defineEmits(['inputCommand', 'cdCommand',]) +const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data',]) const socket = ref(null) // const commandHistoryList = ref([]) const term = ref(null) const command = ref('') const timer = ref(null) +const pingTimer = ref(null) const fitAddon = ref(null) const searchBar = ref(null) const hasRegisterEvent = ref(false) @@ -70,6 +71,7 @@ const fontSize = computed(() => props.fontSize) const background = computed(() => props.background) const hostObj = computed(() => props.hostObj) const hostId = computed(() => hostObj.value.id) +const host = computed(() => hostObj.value.host) let menuCollapse = computed(() => $store.menuCollapse) watch(menuCollapse, () => { @@ -126,6 +128,7 @@ const connectIO = () => { }) socket.value.on('connect', () => { console.log('/terminal socket已连接:', hostId.value) + socketConnected.value = true socket.value.emit('create', { hostId: hostId.value, token: token.value }) socket.value.on('connect_terminal_success', () => { @@ -151,6 +154,14 @@ const connectIO = () => { // }) }) + pingTimer.value = setInterval(() => { + socket.value.emit('get_ping', host.value) + }, 3000) + socket.value.emit('get_ping', host.value) // 获取服务端到客户端的ping值 + socket.value.on('ping_data', (pingMs) => { + emit('ping-data', Object.assign({ ip: host.value }, pingMs)) + }) + socket.value.on('token_verify_fail', () => { $notification({ title: 'Error', message: 'token校验失败,请重新登录', type: 'error' }) $router.push('/login') @@ -412,6 +423,7 @@ onMounted(async () => { onBeforeUnmount(() => { socket.value?.close() window.removeEventListener('resize', handleResize) + clearInterval(pingTimer.value) }) defineExpose({ diff --git a/web/src/views/terminal/components/terminal.vue b/web/src/views/terminal/components/terminal.vue index eacb303..ba416cb 100644 --- a/web/src/views/terminal/components/terminal.vue +++ b/web/src/views/terminal/components/terminal.vue @@ -104,6 +104,7 @@ v-model:show-input-command="showInputCommand" :host-info="curHost" :visible="visible" + :ping-data="pingData" @click-input-command="clickInputCommand" />
@@ -138,6 +139,7 @@ :font-size="terminalFontSize" @input-command="terminalInput" @cd-command="cdCommand" + @ping-data="getPingData" /> { } } +const getPingData = (data) => { + pingData.value[data.ip] = data +} + const tabChange = async (index) => { await $nextTick() const curTerminalRef = terminalRefs.value[index] diff --git a/yarn.lock b/yarn.lock index e41e50c..86b8906 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2796,6 +2796,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -4022,7 +4029,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -"safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==