终端展示服务端ping客户端延迟

This commit is contained in:
chaos-zhu 2024-10-14 22:52:49 +08:00
parent d184a8bdaa
commit 94097a1c6d
9 changed files with 120 additions and 32 deletions

View File

@ -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) ## [2.2.6](https://github.com/chaos-zhu/easynode/releases) (2024-10-14)
### Features ### Features

View File

@ -4,7 +4,7 @@ const { verifyAuthSync } = require('../utils/verify-auth')
const { AESDecryptSync } = require('../utils/encrypt') const { AESDecryptSync } = require('../utils/encrypt')
const { readSSHRecord, readHostList } = require('../utils/storage') const { readSSHRecord, readHostList } = require('../utils/storage')
const { asyncSendNotice } = require('../utils/notify') const { asyncSendNotice } = require('../utils/notify')
const { isAllowedIp } = require('../utils/tools') const { isAllowedIp, ping } = require('../utils/tools')
function createInteractiveShell(socket, sshClient) { function createInteractiveShell(socket, sshClient) {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -98,6 +98,7 @@ async function createTerminal(hostId, socket, sshClient) {
...authInfo ...authInfo
// debug: (info) => console.log(info) // debug: (info) => console.log(info)
}) })
} catch (err) { } catch (err) {
consola.error('创建终端失败: ', host, err.message) consola.error('创建终端失败: ', host, err.message)
socket.emit('create_fail', err.message) socket.emit('create_fail', err.message)
@ -147,6 +148,7 @@ module.exports = (httpServer) => {
} }
socket.on('input', listenerInput) socket.on('input', listenerInput)
socket.on('resize', resizeShell) socket.on('resize', resizeShell)
// 重连 // 重连
socket.on('reconnect_terminal', async () => { socket.on('reconnect_terminal', async () => {
consola.info('重连终端: ', hostId) consola.info('重连终端: ', hostId)
@ -168,6 +170,14 @@ module.exports = (httpServer) => {
stream = await createTerminal(hostId, socket, sshClient) 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) => { socket.on('disconnect', (reason) => {
consola.info('终端socket连接断开:', reason) consola.info('终端socket连接断开:', reason)
}) })

View File

@ -1,4 +1,7 @@
const { exec } = require('child_process')
const os = require('os')
const net = require('net') const net = require('net')
const iconv = require('iconv-lite')
const axios = require('axios') const axios = require('axios')
const request = axios.create({ timeout: 3000 }) const request = axios.create({ timeout: 3000 })
@ -240,6 +243,45 @@ const isAllowedIp = (requestIP) => {
return flag 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 = { module.exports = {
getNetIPInfo, getNetIPInfo,
throwError, throwError,
@ -250,5 +292,6 @@ module.exports = {
resolvePath, resolvePath,
shellThrottle, shellThrottle,
isProd, isProd,
isAllowedIp isAllowedIp,
ping
} }

View File

@ -28,6 +28,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"global": "^4.4.0", "global": "^4.4.0",
"iconv-lite": "^0.6.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"koa": "^2.15.3", "koa": "^2.15.3",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",

View File

@ -299,7 +299,7 @@ const formField = {
password: '', password: '',
privateKey: '', privateKey: '',
credential: '', // credentials -> _id credential: '', // credentials -> _id
clientPort: '22022', clientPort: 22022,
index: 0, index: 0,
expired: null, expired: null,
expiredNotify: false, expiredNotify: false,

View File

@ -14,7 +14,10 @@
</div> </div>
</template> </template>
<span style="margin-right: 10px;">{{ host }}</span> <span style="margin-right: 10px;">{{ host }}</span>
<el-tag size="small" style="cursor: pointer;" @click="handleCopy">复制</el-tag> <template v-if="pingMs">
<span class="host-ping" :style="{backgroundColor: handlePingColor(pingMs)}">{{ pingMs }}ms</span>
</template>
<el-tag size="small" style="cursor: pointer;margin-left: 15px;" @click="handleCopy">复制</el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item> <el-descriptions-item>
<template #label> <template #label>
@ -22,16 +25,15 @@
位置 位置
</div> </div>
</template> </template>
<!-- <div size="small">{{ ipInfo.country || '--' }} {{ ipInfo.regionName }} {{ ipInfo.city }}</div> -->
<div size="small">{{ ipInfo.country || '--' }} {{ ipInfo.regionName }}</div> <div size="small">{{ ipInfo.country || '--' }} {{ ipInfo.regionName }}</div>
</el-descriptions-item> </el-descriptions-item>
<!-- <el-descriptions-item> <!-- <el-descriptions-item v-if="pingMs">
<template #label> <template #label>
<div class="item-title"> <div class="item-title">
延迟 延迟
</div> </div>
</template> </template>
<span style="margin-right: 10px;" class="host-ping">{{ ping }}</span> <span style="margin-right: 10px;" class="host-ping">{{ pingMs }}</span>
</el-descriptions-item> --> </el-descriptions-item> -->
</el-descriptions> </el-descriptions>
@ -54,7 +56,7 @@
:text-inside="true" :text-inside="true"
:stroke-width="18" :stroke-width="18"
:percentage="cpuUsage" :percentage="cpuUsage"
:color="handleColor(cpuUsage)" :color="handleUsedColor(cpuUsage)"
/> />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item> <el-descriptions-item>
@ -67,7 +69,7 @@
:text-inside="true" :text-inside="true"
:stroke-width="18" :stroke-width="18"
:percentage="usedMemPercentage" :percentage="usedMemPercentage"
:color="handleColor(usedMemPercentage)" :color="handleUsedColor(usedMemPercentage)"
/> />
<div class="position-right"> <div class="position-right">
{{ $tools.toFixed(memInfo.usedMemMb / 1024) }}/{{ $tools.toFixed(memInfo.totalMemMb / 1024) }}G {{ $tools.toFixed(memInfo.usedMemMb / 1024) }}/{{ $tools.toFixed(memInfo.totalMemMb / 1024) }}G
@ -83,7 +85,7 @@
:text-inside="true" :text-inside="true"
:stroke-width="18" :stroke-width="18"
:percentage="swapPercentage" :percentage="swapPercentage"
:color="handleColor(swapPercentage)" :color="handleUsedColor(swapPercentage)"
/> />
<div class="position-right"> <div class="position-right">
{{ $tools.toFixed(swapInfo.swapUsed / 1024) }}/{{ $tools.toFixed(swapInfo.swapTotal / 1024) }}G {{ $tools.toFixed(swapInfo.swapUsed / 1024) }}/{{ $tools.toFixed(swapInfo.swapTotal / 1024) }}G
@ -99,7 +101,7 @@
:text-inside="true" :text-inside="true"
:stroke-width="18" :stroke-width="18"
:percentage="usedPercentage" :percentage="usedPercentage"
:color="handleColor(usedPercentage)" :color="handleUsedColor(usedPercentage)"
/> />
<div class="position-right"> <div class="position-right">
{{ driveInfo.usedGb || '--' }}/{{ driveInfo.totalGb || '--' }}G {{ driveInfo.usedGb || '--' }}/{{ driveInfo.totalGb || '--' }}G
@ -229,18 +231,18 @@ const props = defineProps({
showInputCommand: { showInputCommand: {
required: true, required: true,
type: Boolean type: Boolean
},
pingData: {
required: true,
type: Object
} }
}) })
const emit = defineEmits(['update:inputCommandStyle', 'connect-sftp', 'click-input-command',]) const emit = defineEmits(['update:inputCommandStyle', 'connect-sftp', 'click-input-command',])
const socket = ref(null) const socket = ref(null)
// const name = ref('')
const ping = ref(0)
const pingTimer = ref(null) const pingTimer = ref(null)
// const sftpStatus = ref(false)
// const token = computed(() => $store.token)
const hostData = computed(() => props.hostInfo.monitorData || {}) const hostData = computed(() => props.hostInfo.monitorData || {})
const host = computed(() => props.hostInfo.host) const host = computed(() => props.hostInfo.host)
const ipInfo = computed(() => hostData.value?.ipInfo || {}) 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 = () => { // const handleSftp = () => {
// sftpStatus.value = !sftpStatus.value // sftpStatus.value = !sftpStatus.value
// emit('connect-sftp', sftpStatus.value) // emit('connect-sftp', sftpStatus.value)
@ -295,23 +303,17 @@ const handleCopy = async () => {
$message.success({ message: 'success', center: true }) $message.success({ message: 'success', center: true })
} }
const handleColor = (num) => { const handleUsedColor = (num) => {
if (num < 60) return '#13ce66' if (num < 60) return '#13ce66'
if (num < 80) return '#e6a23c' if (num < 80) return '#e6a23c'
if (num <= 100) return '#ff4949' if (num <= 100) return '#ff4949'
} }
// const getHostPing = () => { const handlePingColor = (num) => {
// pingTimer.value = setInterval(() => { if (num < 100) return 'rgba(19, 206, 102, 0.5)' // #13ce66
// $tools.ping(`http://${ props.host }:22022`) if (num < 250) return 'rgba(230, 162, 60, 0.5)' // #e6a23c
// .then(res => { return 'rgba(255, 73, 73, 0.5)' // #ff4949
// ping.value = res }
// if (!import.meta.env.DEV) {
// console.warn('Please tick \'Preserve Log\'')
// }
// })
// }, 3000)
// }
onBeforeUnmount(() => { onBeforeUnmount(() => {
socket.value && socket.value.close() socket.value && socket.value.close()
@ -351,10 +353,9 @@ onBeforeUnmount(() => {
.host-ping { .host-ping {
display: inline-block; display: inline-block;
font-size: 13px; font-size: 10px;
color: #009933;
background-color: #e8fff3;
padding: 0 5px; padding: 0 5px;
border-radius: 2px;
} }
// 线title // 线title

View File

@ -48,13 +48,14 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['inputCommand', 'cdCommand',]) const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data',])
const socket = ref(null) const socket = ref(null)
// const commandHistoryList = ref([]) // const commandHistoryList = ref([])
const term = ref(null) const term = ref(null)
const command = ref('') const command = ref('')
const timer = ref(null) const timer = ref(null)
const pingTimer = ref(null)
const fitAddon = ref(null) const fitAddon = ref(null)
const searchBar = ref(null) const searchBar = ref(null)
const hasRegisterEvent = ref(false) const hasRegisterEvent = ref(false)
@ -70,6 +71,7 @@ const fontSize = computed(() => props.fontSize)
const background = computed(() => props.background) const background = computed(() => props.background)
const hostObj = computed(() => props.hostObj) const hostObj = computed(() => props.hostObj)
const hostId = computed(() => hostObj.value.id) const hostId = computed(() => hostObj.value.id)
const host = computed(() => hostObj.value.host)
let menuCollapse = computed(() => $store.menuCollapse) let menuCollapse = computed(() => $store.menuCollapse)
watch(menuCollapse, () => { watch(menuCollapse, () => {
@ -126,6 +128,7 @@ const connectIO = () => {
}) })
socket.value.on('connect', () => { socket.value.on('connect', () => {
console.log('/terminal socket已连接', hostId.value) console.log('/terminal socket已连接', hostId.value)
socketConnected.value = true socketConnected.value = true
socket.value.emit('create', { hostId: hostId.value, token: token.value }) socket.value.emit('create', { hostId: hostId.value, token: token.value })
socket.value.on('connect_terminal_success', () => { 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', () => { socket.value.on('token_verify_fail', () => {
$notification({ title: 'Error', message: 'token校验失败请重新登录', type: 'error' }) $notification({ title: 'Error', message: 'token校验失败请重新登录', type: 'error' })
$router.push('/login') $router.push('/login')
@ -412,6 +423,7 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
socket.value?.close() socket.value?.close()
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
clearInterval(pingTimer.value)
}) })
defineExpose({ defineExpose({

View File

@ -104,6 +104,7 @@
v-model:show-input-command="showInputCommand" v-model:show-input-command="showInputCommand"
:host-info="curHost" :host-info="curHost"
:visible="visible" :visible="visible"
:ping-data="pingData"
@click-input-command="clickInputCommand" @click-input-command="clickInputCommand"
/> />
</div> </div>
@ -138,6 +139,7 @@
:font-size="terminalFontSize" :font-size="terminalFontSize"
@input-command="terminalInput" @input-command="terminalInput"
@cd-command="cdCommand" @cd-command="cdCommand"
@ping-data="getPingData"
/> />
<Sftp <Sftp
v-if="showSftp" v-if="showSftp"
@ -191,6 +193,7 @@ const emit = defineEmits(['closed', 'close-all-tab', 'removeTab', 'add-host',])
const showInputCommand = ref(false) const showInputCommand = ref(false)
const infoSideRef = ref(null) const infoSideRef = ref(null)
const pingData = ref({})
const terminalRefs = ref([]) const terminalRefs = ref([])
const sftpRefs = ref([]) const sftpRefs = ref([])
const activeTabIndex = ref(0) const activeTabIndex = ref(0)
@ -302,6 +305,10 @@ const cdCommand = (path) => {
} }
} }
const getPingData = (data) => {
pingData.value[data.ip] = data
}
const tabChange = async (index) => { const tabChange = async (index) => {
await $nextTick() await $nextTick()
const curTerminalRef = terminalRefs.value[index] const curTerminalRef = terminalRefs.value[index]

View File

@ -2796,6 +2796,13 @@ iconv-lite@0.4.24:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3" 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: ieee754@^1.1.13:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 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" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 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" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==