✨ 终端展示服务端ping客户端延迟
This commit is contained in:
parent
d184a8bdaa
commit
94097a1c6d
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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
|
||||
}
|
@ -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",
|
||||
|
@ -299,7 +299,7 @@ const formField = {
|
||||
password: '',
|
||||
privateKey: '',
|
||||
credential: '', // credentials -> _id
|
||||
clientPort: '22022',
|
||||
clientPort: 22022,
|
||||
index: 0,
|
||||
expired: null,
|
||||
expiredNotify: false,
|
||||
|
@ -14,7 +14,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<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>
|
||||
<template #label>
|
||||
@ -22,16 +25,15 @@
|
||||
位置
|
||||
</div>
|
||||
</template>
|
||||
<!-- <div size="small">{{ ipInfo.country || '--' }} {{ ipInfo.regionName }} {{ ipInfo.city }}</div> -->
|
||||
<div size="small">{{ ipInfo.country || '--' }} {{ ipInfo.regionName }}</div>
|
||||
</el-descriptions-item>
|
||||
<!-- <el-descriptions-item>
|
||||
<!-- <el-descriptions-item v-if="pingMs">
|
||||
<template #label>
|
||||
<div class="item-title">
|
||||
延迟
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@ -54,7 +56,7 @@
|
||||
:text-inside="true"
|
||||
:stroke-width="18"
|
||||
:percentage="cpuUsage"
|
||||
:color="handleColor(cpuUsage)"
|
||||
:color="handleUsedColor(cpuUsage)"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
@ -67,7 +69,7 @@
|
||||
:text-inside="true"
|
||||
:stroke-width="18"
|
||||
:percentage="usedMemPercentage"
|
||||
:color="handleColor(usedMemPercentage)"
|
||||
:color="handleUsedColor(usedMemPercentage)"
|
||||
/>
|
||||
<div class="position-right">
|
||||
{{ $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)"
|
||||
/>
|
||||
<div class="position-right">
|
||||
{{ $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)"
|
||||
/>
|
||||
<div class="position-right">
|
||||
{{ 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
|
||||
|
@ -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({
|
||||
|
@ -104,6 +104,7 @@
|
||||
v-model:show-input-command="showInputCommand"
|
||||
:host-info="curHost"
|
||||
:visible="visible"
|
||||
:ping-data="pingData"
|
||||
@click-input-command="clickInputCommand"
|
||||
/>
|
||||
</div>
|
||||
@ -138,6 +139,7 @@
|
||||
:font-size="terminalFontSize"
|
||||
@input-command="terminalInput"
|
||||
@cd-command="cdCommand"
|
||||
@ping-data="getPingData"
|
||||
/>
|
||||
<Sftp
|
||||
v-if="showSftp"
|
||||
@ -191,6 +193,7 @@ const emit = defineEmits(['closed', 'close-all-tab', 'removeTab', 'add-host',])
|
||||
|
||||
const showInputCommand = ref(false)
|
||||
const infoSideRef = ref(null)
|
||||
const pingData = ref({})
|
||||
const terminalRefs = ref([])
|
||||
const sftpRefs = ref([])
|
||||
const activeTabIndex = ref(0)
|
||||
@ -302,6 +305,10 @@ const cdCommand = (path) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getPingData = (data) => {
|
||||
pingData.value[data.ip] = data
|
||||
}
|
||||
|
||||
const tabChange = async (index) => {
|
||||
await $nextTick()
|
||||
const curTerminalRef = terminalRefs.value[index]
|
||||
|
@ -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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user